diff --git a/collab-editor-accessibility-parity-guard/README.md b/collab-editor-accessibility-parity-guard/README.md
new file mode 100644
index 0000000..6e5270a
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/README.md
@@ -0,0 +1,32 @@
+# Collaborative Editor Accessibility Parity Guard
+
+This module adds a focused accessibility and input-mode parity gate for issue #12, the real-time collaborative research editor.
+
+It checks whether collaborative editor surfaces are usable across Markdown and WYSIWYG modes before a manuscript is released to reviewers or coauthors.
+
+## What It Checks
+
+- keyboard reachability for toolbars, comments, suggestions, locks, and notebook cells
+- screen-reader labels for interactive controls
+- spoken text for LaTeX equation blocks
+- alt text for visual notebook outputs
+- anchors for comments and suggestions
+- screen-reader announcements for section lock state changes
+- focus order coverage and unknown focus targets
+- Markdown/WYSIWYG availability parity for shared controls
+
+## Run
+
+```bash
+node collab-editor-accessibility-parity-guard/test.js
+node collab-editor-accessibility-parity-guard/demo.js
+```
+
+The demo writes:
+
+- `reports/accessibility-parity-packet.json`
+- `reports/accessibility-parity-report.md`
+- `reports/summary.svg`
+- `reports/demo.mp4`
+
+Synthetic data only. No credentials, external APIs, or browser automation are required.
diff --git a/collab-editor-accessibility-parity-guard/acceptance-notes.md b/collab-editor-accessibility-parity-guard/acceptance-notes.md
new file mode 100644
index 0000000..c7f4e6a
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/acceptance-notes.md
@@ -0,0 +1,38 @@
+# Acceptance Notes
+
+This PR is a narrow accessibility and input-mode parity slice for issue #12.
+
+## Distinctness
+
+It avoids duplicating prior real-time editor submissions around:
+
+- broad editor foundations
+- operation replay
+- offline conflict resolution
+- notebook workbenches
+- reference formatting
+- authorship governance
+- freeze, lock, and autosave recovery lanes
+- discussion sidebar audit
+- round-trip fidelity
+- review decision ledger
+- task dependency guard
+- equation and figure anchor guards
+- notebook kernel lease guard
+- collaborative presence privacy
+
+## Verification
+
+Expected local checks:
+
+```bash
+node collab-editor-accessibility-parity-guard/test.js
+node collab-editor-accessibility-parity-guard/demo.js
+node --check collab-editor-accessibility-parity-guard/index.js collab-editor-accessibility-parity-guard/sample-data.js collab-editor-accessibility-parity-guard/test.js collab-editor-accessibility-parity-guard/demo.js
+git diff --check
+ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 collab-editor-accessibility-parity-guard/reports/demo.mp4
+```
+
+## Safety
+
+The module uses synthetic data only and does not execute notebook code, connect to a browser session, or call any external service.
diff --git a/collab-editor-accessibility-parity-guard/demo.js b/collab-editor-accessibility-parity-guard/demo.js
new file mode 100644
index 0000000..312e2b6
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/demo.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const sampleData = require('./sample-data');
+const {
+ evaluateAccessibilityParity,
+ toMarkdownReport,
+ toSvgSummary
+} = require('./index');
+
+const reportsDir = path.join(__dirname, 'reports');
+fs.mkdirSync(reportsDir, { recursive: true });
+
+const result = evaluateAccessibilityParity(sampleData);
+
+fs.writeFileSync(
+ path.join(reportsDir, 'accessibility-parity-packet.json'),
+ `${JSON.stringify(result, null, 2)}\n`
+);
+fs.writeFileSync(
+ path.join(reportsDir, 'accessibility-parity-report.md'),
+ toMarkdownReport(result)
+);
+fs.writeFileSync(
+ path.join(reportsDir, 'summary.svg'),
+ toSvgSummary(result)
+);
+
+console.log(`status=${result.status}, blockers=${result.summary.blockers.length}, warnings=${result.summary.warnings.length}`);
diff --git a/collab-editor-accessibility-parity-guard/index.js b/collab-editor-accessibility-parity-guard/index.js
new file mode 100644
index 0000000..7eaff9a
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/index.js
@@ -0,0 +1,235 @@
+'use strict';
+
+const DEFAULT_NOW = '2026-05-21T10:45:00.000Z';
+const REQUIRED_MODES = ['markdown', 'wysiwyg'];
+const INTERACTIVE_TYPES = new Set(['toolbar', 'comment', 'suggestion', 'lock', 'notebook-cell']);
+
+function listMissing(object, fields) {
+ return fields.filter((field) => object[field] === undefined || object[field] === null || object[field] === '');
+}
+
+function hasModeParity(control) {
+ const modes = new Set(control.availableInModes || []);
+ return REQUIRED_MODES.every((mode) => modes.has(mode));
+}
+
+function evaluateControl(control) {
+ const blockers = [];
+ const warnings = [];
+ const actions = [];
+
+ const missing = listMissing(control, ['id', 'type', 'label']);
+ if (missing.length > 0) {
+ blockers.push(`missing required fields: ${missing.join(', ')}`);
+ actions.push('complete control metadata before exposing the editor surface');
+ }
+
+ if (INTERACTIVE_TYPES.has(control.type)) {
+ if (!control.ariaLabel) {
+ blockers.push('interactive control has no aria label');
+ actions.push(`add an aria label to ${control.id}`);
+ }
+ if (!control.keyboardShortcut && !control.tabReachable) {
+ blockers.push('interactive control is not keyboard reachable');
+ actions.push(`make ${control.id} reachable by tab order or shortcut`);
+ }
+ if (!hasModeParity(control)) {
+ warnings.push('control is not available in both Markdown and WYSIWYG modes');
+ actions.push(`add mode parity for ${control.id}`);
+ }
+ }
+
+ if (control.type === 'equation' && !control.accessibleText) {
+ blockers.push('equation block has no accessible text');
+ actions.push(`add spoken math text for ${control.id}`);
+ }
+
+ if (control.type === 'notebook-cell' && control.outputKind !== 'text' && !control.outputAltText) {
+ blockers.push('notebook visual output has no alt text');
+ actions.push(`add output alt text for notebook cell ${control.id}`);
+ }
+
+ if ((control.type === 'comment' || control.type === 'suggestion') && !control.anchorId) {
+ blockers.push('review annotation has no document anchor');
+ actions.push(`anchor ${control.id} to a manuscript block or notebook cell`);
+ }
+
+ if (control.type === 'lock' && !control.screenReaderAnnouncement) {
+ warnings.push('section lock does not announce state changes');
+ actions.push(`add lock/unlock announcement text for ${control.id}`);
+ }
+
+ return {
+ id: control.id,
+ type: control.type,
+ label: control.label,
+ blockers,
+ warnings,
+ actions
+ };
+}
+
+function evaluateFocusOrder(focusOrder, controls) {
+ const blockers = [];
+ const warnings = [];
+ const actions = [];
+ const controlIds = new Set(controls.map((control) => control.id));
+ const seen = new Set();
+
+ for (const id of focusOrder || []) {
+ if (!controlIds.has(id)) {
+ blockers.push(`focus order references unknown control ${id}`);
+ actions.push(`remove or define focus target ${id}`);
+ }
+ if (seen.has(id)) {
+ warnings.push(`duplicate focus target ${id}`);
+ actions.push(`deduplicate focus target ${id}`);
+ }
+ seen.add(id);
+ }
+
+ for (const control of controls) {
+ if (INTERACTIVE_TYPES.has(control.type) && !seen.has(control.id) && !control.keyboardShortcut) {
+ blockers.push(`interactive control ${control.id} is missing from focus order`);
+ actions.push(`add ${control.id} to the focus order`);
+ }
+ }
+
+ return { blockers, warnings, actions };
+}
+
+function summarize(controlReviews, focusReview) {
+ const blockers = [];
+ const warnings = [];
+ const actions = [];
+
+ for (const review of controlReviews) {
+ for (const blocker of review.blockers) {
+ blockers.push({ controlId: review.id, message: blocker });
+ }
+ for (const warning of review.warnings) {
+ warnings.push({ controlId: review.id, message: warning });
+ }
+ for (const action of review.actions) {
+ actions.push({ controlId: review.id, action });
+ }
+ }
+
+ for (const blocker of focusReview.blockers) {
+ blockers.push({ controlId: 'focus-order', message: blocker });
+ }
+ for (const warning of focusReview.warnings) {
+ warnings.push({ controlId: 'focus-order', message: warning });
+ }
+ for (const action of focusReview.actions) {
+ actions.push({ controlId: 'focus-order', action });
+ }
+
+ return {
+ status: blockers.length > 0
+ ? 'hold_accessibility_release'
+ : warnings.length > 0
+ ? 'accessibility_review_needed'
+ : 'accessibility_ready',
+ blockers,
+ warnings,
+ actions
+ };
+}
+
+function evaluateAccessibilityParity(input, options = {}) {
+ const generatedAt = options.now || input.generatedAt || DEFAULT_NOW;
+ const controlReviews = input.controls.map(evaluateControl);
+ const focusReview = evaluateFocusOrder(input.focusOrder, input.controls);
+ const summary = summarize(controlReviews, focusReview);
+
+ return {
+ documentId: input.documentId,
+ documentTitle: input.documentTitle,
+ generatedAt,
+ status: summary.status,
+ modePolicy: REQUIRED_MODES,
+ summary,
+ controlReviews,
+ focusReview
+ };
+}
+
+function toMarkdownReport(result) {
+ const lines = [
+ '# Collaborative Editor Accessibility Parity Guard Report',
+ '',
+ `Document: ${result.documentTitle} (${result.documentId})`,
+ `Status: ${result.status}`,
+ `Generated: ${result.generatedAt}`,
+ '',
+ '## Summary',
+ '',
+ `- Controls reviewed: ${result.controlReviews.length}`,
+ `- Blockers: ${result.summary.blockers.length}`,
+ `- Warnings: ${result.summary.warnings.length}`,
+ `- Required modes: ${result.modePolicy.join(', ')}`,
+ ''
+ ];
+
+ if (result.summary.blockers.length > 0) {
+ lines.push('## Blockers', '');
+ for (const blocker of result.summary.blockers) {
+ lines.push(`- ${blocker.controlId}: ${blocker.message}`);
+ }
+ lines.push('');
+ }
+
+ if (result.summary.warnings.length > 0) {
+ lines.push('## Warnings', '');
+ for (const warning of result.summary.warnings) {
+ lines.push(`- ${warning.controlId}: ${warning.message}`);
+ }
+ lines.push('');
+ }
+
+ lines.push('## Required Actions', '');
+ for (const action of result.summary.actions) {
+ lines.push(`- ${action.controlId}: ${action.action}`);
+ }
+ lines.push('');
+
+ lines.push('## Control Matrix', '');
+ for (const review of result.controlReviews) {
+ lines.push(`- ${review.id} (${review.type}): ${review.blockers.length} blockers, ${review.warnings.length} warnings`);
+ }
+
+ return `${lines.join('\n')}\n`;
+}
+
+function xmlEscape(value) {
+ return String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+function toSvgSummary(result) {
+ const statusColor = result.status === 'accessibility_ready' ? '#0f766e' : '#b91c1c';
+ const statusLabel = result.status === 'accessibility_ready' ? 'READY' : 'HOLD';
+ return `
+`;
+}
+
+module.exports = {
+ evaluateAccessibilityParity,
+ toMarkdownReport,
+ toSvgSummary
+};
diff --git a/collab-editor-accessibility-parity-guard/reports/accessibility-parity-packet.json b/collab-editor-accessibility-parity-guard/reports/accessibility-parity-packet.json
new file mode 100644
index 0000000..bc80318
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/reports/accessibility-parity-packet.json
@@ -0,0 +1,176 @@
+{
+ "documentId": "ms-collab-editor-demo",
+ "documentTitle": "Shared CRISPR off-target analysis manuscript",
+ "generatedAt": "2026-05-21T10:45:00.000Z",
+ "status": "hold_accessibility_release",
+ "modePolicy": [
+ "markdown",
+ "wysiwyg"
+ ],
+ "summary": {
+ "status": "hold_accessibility_release",
+ "blockers": [
+ {
+ "controlId": "equation-effect-size",
+ "message": "equation block has no accessible text"
+ },
+ {
+ "controlId": "notebook-cell-analysis-01",
+ "message": "notebook visual output has no alt text"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "message": "interactive control has no aria label"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "message": "interactive control is not keyboard reachable"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "message": "review annotation has no document anchor"
+ },
+ {
+ "controlId": "focus-order",
+ "message": "focus order references unknown control missing-control"
+ }
+ ],
+ "warnings": [
+ {
+ "controlId": "notebook-cell-analysis-01",
+ "message": "control is not available in both Markdown and WYSIWYG modes"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "message": "control is not available in both Markdown and WYSIWYG modes"
+ },
+ {
+ "controlId": "section-lock-final-figures",
+ "message": "section lock does not announce state changes"
+ }
+ ],
+ "actions": [
+ {
+ "controlId": "equation-effect-size",
+ "action": "add spoken math text for equation-effect-size"
+ },
+ {
+ "controlId": "notebook-cell-analysis-01",
+ "action": "add mode parity for notebook-cell-analysis-01"
+ },
+ {
+ "controlId": "notebook-cell-analysis-01",
+ "action": "add output alt text for notebook cell notebook-cell-analysis-01"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "action": "add an aria label to suggestion-results-01"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "action": "make suggestion-results-01 reachable by tab order or shortcut"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "action": "add mode parity for suggestion-results-01"
+ },
+ {
+ "controlId": "suggestion-results-01",
+ "action": "anchor suggestion-results-01 to a manuscript block or notebook cell"
+ },
+ {
+ "controlId": "section-lock-final-figures",
+ "action": "add lock/unlock announcement text for section-lock-final-figures"
+ },
+ {
+ "controlId": "focus-order",
+ "action": "remove or define focus target missing-control"
+ }
+ ]
+ },
+ "controlReviews": [
+ {
+ "id": "toolbar-insert-equation",
+ "type": "toolbar",
+ "label": "Insert equation",
+ "blockers": [],
+ "warnings": [],
+ "actions": []
+ },
+ {
+ "id": "equation-effect-size",
+ "type": "equation",
+ "label": "Effect size equation",
+ "blockers": [
+ "equation block has no accessible text"
+ ],
+ "warnings": [],
+ "actions": [
+ "add spoken math text for equation-effect-size"
+ ]
+ },
+ {
+ "id": "notebook-cell-analysis-01",
+ "type": "notebook-cell",
+ "label": "Off-target summary plot cell",
+ "blockers": [
+ "notebook visual output has no alt text"
+ ],
+ "warnings": [
+ "control is not available in both Markdown and WYSIWYG modes"
+ ],
+ "actions": [
+ "add mode parity for notebook-cell-analysis-01",
+ "add output alt text for notebook cell notebook-cell-analysis-01"
+ ]
+ },
+ {
+ "id": "comment-methods-01",
+ "type": "comment",
+ "label": "Methods reviewer comment",
+ "blockers": [],
+ "warnings": [],
+ "actions": []
+ },
+ {
+ "id": "suggestion-results-01",
+ "type": "suggestion",
+ "label": "Results wording suggestion",
+ "blockers": [
+ "interactive control has no aria label",
+ "interactive control is not keyboard reachable",
+ "review annotation has no document anchor"
+ ],
+ "warnings": [
+ "control is not available in both Markdown and WYSIWYG modes"
+ ],
+ "actions": [
+ "add an aria label to suggestion-results-01",
+ "make suggestion-results-01 reachable by tab order or shortcut",
+ "add mode parity for suggestion-results-01",
+ "anchor suggestion-results-01 to a manuscript block or notebook cell"
+ ]
+ },
+ {
+ "id": "section-lock-final-figures",
+ "type": "lock",
+ "label": "Final figures section lock",
+ "blockers": [],
+ "warnings": [
+ "section lock does not announce state changes"
+ ],
+ "actions": [
+ "add lock/unlock announcement text for section-lock-final-figures"
+ ]
+ }
+ ],
+ "focusReview": {
+ "blockers": [
+ "focus order references unknown control missing-control"
+ ],
+ "warnings": [],
+ "actions": [
+ "remove or define focus target missing-control"
+ ]
+ }
+}
diff --git a/collab-editor-accessibility-parity-guard/reports/accessibility-parity-report.md b/collab-editor-accessibility-parity-guard/reports/accessibility-parity-report.md
new file mode 100644
index 0000000..7503520
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/reports/accessibility-parity-report.md
@@ -0,0 +1,48 @@
+# Collaborative Editor Accessibility Parity Guard Report
+
+Document: Shared CRISPR off-target analysis manuscript (ms-collab-editor-demo)
+Status: hold_accessibility_release
+Generated: 2026-05-21T10:45:00.000Z
+
+## Summary
+
+- Controls reviewed: 6
+- Blockers: 6
+- Warnings: 3
+- Required modes: markdown, wysiwyg
+
+## Blockers
+
+- equation-effect-size: equation block has no accessible text
+- notebook-cell-analysis-01: notebook visual output has no alt text
+- suggestion-results-01: interactive control has no aria label
+- suggestion-results-01: interactive control is not keyboard reachable
+- suggestion-results-01: review annotation has no document anchor
+- focus-order: focus order references unknown control missing-control
+
+## Warnings
+
+- notebook-cell-analysis-01: control is not available in both Markdown and WYSIWYG modes
+- suggestion-results-01: control is not available in both Markdown and WYSIWYG modes
+- section-lock-final-figures: section lock does not announce state changes
+
+## Required Actions
+
+- equation-effect-size: add spoken math text for equation-effect-size
+- notebook-cell-analysis-01: add mode parity for notebook-cell-analysis-01
+- notebook-cell-analysis-01: add output alt text for notebook cell notebook-cell-analysis-01
+- suggestion-results-01: add an aria label to suggestion-results-01
+- suggestion-results-01: make suggestion-results-01 reachable by tab order or shortcut
+- suggestion-results-01: add mode parity for suggestion-results-01
+- suggestion-results-01: anchor suggestion-results-01 to a manuscript block or notebook cell
+- section-lock-final-figures: add lock/unlock announcement text for section-lock-final-figures
+- focus-order: remove or define focus target missing-control
+
+## Control Matrix
+
+- toolbar-insert-equation (toolbar): 0 blockers, 0 warnings
+- equation-effect-size (equation): 1 blockers, 0 warnings
+- notebook-cell-analysis-01 (notebook-cell): 1 blockers, 1 warnings
+- comment-methods-01 (comment): 0 blockers, 0 warnings
+- suggestion-results-01 (suggestion): 3 blockers, 1 warnings
+- section-lock-final-figures (lock): 0 blockers, 1 warnings
diff --git a/collab-editor-accessibility-parity-guard/reports/demo.mp4 b/collab-editor-accessibility-parity-guard/reports/demo.mp4
new file mode 100644
index 0000000..300bed9
Binary files /dev/null and b/collab-editor-accessibility-parity-guard/reports/demo.mp4 differ
diff --git a/collab-editor-accessibility-parity-guard/reports/summary.png b/collab-editor-accessibility-parity-guard/reports/summary.png
new file mode 100644
index 0000000..7ec7b24
Binary files /dev/null and b/collab-editor-accessibility-parity-guard/reports/summary.png differ
diff --git a/collab-editor-accessibility-parity-guard/reports/summary.svg b/collab-editor-accessibility-parity-guard/reports/summary.svg
new file mode 100644
index 0000000..141ff38
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/reports/summary.svg
@@ -0,0 +1,12 @@
+
diff --git a/collab-editor-accessibility-parity-guard/requirements-map.md b/collab-editor-accessibility-parity-guard/requirements-map.md
new file mode 100644
index 0000000..bce5233
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/requirements-map.md
@@ -0,0 +1,30 @@
+# Requirements Map
+
+Issue #12 describes a real-time collaborative research editor with rich scientific formatting, Jupyter notebook integration, collaboration controls, versioning, and task workflow. This slice adds an accessibility parity gate across those surfaces.
+
+## Rich Scientific Formatting
+
+- Verifies LaTeX equation blocks include screen-reader friendly spoken text.
+- Checks shared toolbar controls have accessible names and keyboard reachability.
+- Ensures controls are available in both Markdown and WYSIWYG modes.
+
+## Jupyter Notebook Integration
+
+- Checks notebook cells are reachable by keyboard.
+- Requires alt text for visual notebook outputs such as plots and images.
+- Includes notebook controls in the focus-order audit.
+
+## Real-Time Collaboration
+
+- Validates inline comments and suggestions have document anchors.
+- Requires aria labels for collaborative comments and suggestions.
+- Checks section locks announce state changes to assistive technology.
+
+## Version History And Autosave
+
+- Produces a deterministic review packet that can be stored with a named release or autosave checkpoint.
+- Flags focus-order drift before a release snapshot is shared.
+
+## Integrated Task Workflow
+
+- Uses the same metadata pattern for comments, suggestions, and lock-linked work items so blocked controls can be routed into release tasks.
diff --git a/collab-editor-accessibility-parity-guard/sample-data.js b/collab-editor-accessibility-parity-guard/sample-data.js
new file mode 100644
index 0000000..96ad2eb
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/sample-data.js
@@ -0,0 +1,69 @@
+'use strict';
+
+module.exports = {
+ generatedAt: '2026-05-21T10:45:00.000Z',
+ documentId: 'ms-collab-editor-demo',
+ documentTitle: 'Shared CRISPR off-target analysis manuscript',
+ focusOrder: [
+ 'toolbar-insert-equation',
+ 'notebook-cell-analysis-01',
+ 'comment-methods-01',
+ 'suggestion-results-01',
+ 'section-lock-final-figures',
+ 'missing-control'
+ ],
+ controls: [
+ {
+ id: 'toolbar-insert-equation',
+ type: 'toolbar',
+ label: 'Insert equation',
+ ariaLabel: 'Insert LaTeX equation',
+ keyboardShortcut: 'Mod+Shift+E',
+ availableInModes: ['markdown', 'wysiwyg']
+ },
+ {
+ id: 'equation-effect-size',
+ type: 'equation',
+ label: 'Effect size equation',
+ latex: '\\\\Delta = mean(treatment) - mean(control)',
+ accessibleText: ''
+ },
+ {
+ id: 'notebook-cell-analysis-01',
+ type: 'notebook-cell',
+ label: 'Off-target summary plot cell',
+ ariaLabel: 'Notebook cell with off-target summary plot',
+ tabReachable: true,
+ availableInModes: ['markdown'],
+ outputKind: 'plot',
+ outputAltText: ''
+ },
+ {
+ id: 'comment-methods-01',
+ type: 'comment',
+ label: 'Methods reviewer comment',
+ ariaLabel: 'Comment on methods randomization',
+ tabReachable: true,
+ availableInModes: ['markdown', 'wysiwyg'],
+ anchorId: 'methods-randomization'
+ },
+ {
+ id: 'suggestion-results-01',
+ type: 'suggestion',
+ label: 'Results wording suggestion',
+ ariaLabel: '',
+ tabReachable: false,
+ availableInModes: ['wysiwyg'],
+ anchorId: ''
+ },
+ {
+ id: 'section-lock-final-figures',
+ type: 'lock',
+ label: 'Final figures section lock',
+ ariaLabel: 'Lock final figures section',
+ tabReachable: true,
+ availableInModes: ['markdown', 'wysiwyg'],
+ screenReaderAnnouncement: ''
+ }
+ ]
+};
diff --git a/collab-editor-accessibility-parity-guard/test.js b/collab-editor-accessibility-parity-guard/test.js
new file mode 100644
index 0000000..bb8f4cf
--- /dev/null
+++ b/collab-editor-accessibility-parity-guard/test.js
@@ -0,0 +1,73 @@
+'use strict';
+
+const assert = require('assert');
+const sampleData = require('./sample-data');
+const {
+ evaluateAccessibilityParity,
+ toMarkdownReport,
+ toSvgSummary
+} = require('./index');
+
+function testBlocksMissingEquationAccessibleTextAndNotebookAltText() {
+ const result = evaluateAccessibilityParity(sampleData);
+ assert.strictEqual(result.status, 'hold_accessibility_release');
+ assert.ok(result.summary.blockers.some((item) => item.message.includes('equation block has no accessible text')));
+ assert.ok(result.summary.blockers.some((item) => item.message.includes('notebook visual output has no alt text')));
+}
+
+function testBlocksSuggestionWithoutAriaKeyboardOrAnchor() {
+ const result = evaluateAccessibilityParity(sampleData);
+ const suggestion = result.controlReviews.find((review) => review.id === 'suggestion-results-01');
+ assert.ok(suggestion.blockers.some((blocker) => blocker.includes('no aria label')));
+ assert.ok(suggestion.blockers.some((blocker) => blocker.includes('not keyboard reachable')));
+ assert.ok(suggestion.blockers.some((blocker) => blocker.includes('no document anchor')));
+}
+
+function testFocusOrderUnknownTargetBlocksRelease() {
+ const result = evaluateAccessibilityParity(sampleData);
+ assert.ok(result.focusReview.blockers.some((blocker) => blocker.includes('unknown control missing-control')));
+}
+
+function testCleanEditorSurfacePasses() {
+ const clean = {
+ ...sampleData,
+ focusOrder: sampleData.controls.map((control) => control.id),
+ controls: sampleData.controls.map((control) => ({
+ ...control,
+ ariaLabel: control.ariaLabel || `${control.label} control`,
+ tabReachable: true,
+ availableInModes: ['markdown', 'wysiwyg'],
+ accessibleText: control.type === 'equation' ? 'Delta equals treatment mean minus control mean' : control.accessibleText,
+ outputAltText: control.type === 'notebook-cell' ? 'Bar plot comparing off-target counts by condition' : control.outputAltText,
+ anchorId: control.type === 'suggestion' ? 'results-main-effect' : control.anchorId,
+ screenReaderAnnouncement: control.type === 'lock' ? 'Final figures section locked for editing' : control.screenReaderAnnouncement
+ }))
+ };
+ const result = evaluateAccessibilityParity(clean);
+ assert.strictEqual(result.status, 'accessibility_ready');
+ assert.strictEqual(result.summary.blockers.length, 0);
+}
+
+function testReportsRender() {
+ const result = evaluateAccessibilityParity(sampleData);
+ const markdown = toMarkdownReport(result);
+ const svg = toSvgSummary(result);
+ assert.ok(markdown.includes('Collaborative Editor Accessibility Parity Guard Report'));
+ assert.ok(markdown.includes('Control Matrix'));
+ assert.ok(svg.includes('