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 ` + + + Collaborative Editor Accessibility Parity Guard + ${xmlEscape(result.documentTitle)} + + ${statusLabel} + Controls reviewed: ${result.controlReviews.length} + Blockers: ${result.summary.blockers.length} + Warnings: ${result.summary.warnings.length} + Checks keyboard reachability, screen-reader labels, visual output alt text, and Markdown/WYSIWYG parity before collaborative release. + +`; +} + +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 @@ + + + + Collaborative Editor Accessibility Parity Guard + Shared CRISPR off-target analysis manuscript + + HOLD + Controls reviewed: 6 + Blockers: 6 + Warnings: 3 + Checks keyboard reachability, screen-reader labels, visual output alt text, and Markdown/WYSIWYG parity before collaborative release. + 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('