@polymorphism-tech/morph-spec 4.21.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CLAUDE.md +57 -71
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +224 -491
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +55 -38
  8. package/framework/MORPH.md +244 -0
  9. package/framework/agents.json +127 -1183
  10. package/framework/commands/morph-apply.md +56 -240
  11. package/framework/commands/morph-archive.md +48 -85
  12. package/framework/commands/morph-preflight.md +7 -7
  13. package/framework/commands/morph-proposal.md +35 -191
  14. package/framework/commands/morph-status.md +19 -50
  15. package/framework/commands/morph-troubleshoot.md +2 -2
  16. package/framework/evals/rubrics/README.md +44 -0
  17. package/framework/evals/rubrics/architecture.md +46 -0
  18. package/framework/evals/rubrics/code-quality.md +50 -0
  19. package/framework/evals/rubrics/contracts.md +47 -0
  20. package/framework/evals/rubrics/test-coverage.md +50 -0
  21. package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
  22. package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
  23. package/framework/evals/suites/standard.yaml +5 -0
  24. package/framework/evals/trace-schema.md +67 -0
  25. package/framework/hooks/claude-code/core/README.md +3 -0
  26. package/framework/hooks/claude-code/core/denylist.json +14 -0
  27. package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
  28. package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
  30. package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
  31. package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
  32. package/framework/hooks/shared/phase-utils.js +20 -28
  33. package/framework/hooks/shared/state-reader.js +4 -5
  34. package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -210
  35. package/framework/phases.json +40 -114
  36. package/framework/rubrics/eval-rubric.json +66 -0
  37. package/framework/skills/README.md +39 -64
  38. package/framework/skills/core/README.md +3 -0
  39. package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
  40. package/framework/skills/core/morph-eval/SKILL.md +57 -0
  41. package/framework/skills/core/morph-implement/SKILL.md +83 -0
  42. package/framework/skills/core/morph-plan/SKILL.md +64 -0
  43. package/framework/skills/core/morph-review/SKILL.md +62 -0
  44. package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
  45. package/framework/skills/core/morph-standards/SKILL.md +49 -0
  46. package/framework/standards/STANDARDS.json +32 -0
  47. package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
  48. package/framework/standards/integration/mcp/mcp-tools.md +48 -0
  49. package/framework/templates/CLAUDE.md.core.guide.md +29 -0
  50. package/framework/templates/CLAUDE.md.core.template +22 -0
  51. package/framework/templates/REGISTRY.json +4 -4
  52. package/framework/workflows/configs/fast-track.json +5 -10
  53. package/framework/workflows/configs/full.json +4 -8
  54. package/framework/workflows/configs/quick.json +32 -0
  55. package/framework/workflows/configs/standard.json +57 -64
  56. package/package.json +7 -6
  57. package/src/commands/mcp/mcp-setup.js +58 -31
  58. package/src/commands/project/doctor.js +69 -70
  59. package/src/commands/project/init.js +26 -5
  60. package/src/commands/project/status.js +18 -19
  61. package/src/commands/state/approve.js +1 -1
  62. package/src/commands/validation/validate-feature.js +18 -112
  63. package/src/core/paths/output-schema.js +133 -151
  64. package/src/core/state/phase-state-machine.js +10 -24
  65. package/src/core/state/state-manager.js +17 -17
  66. package/src/core/workflows/workflow-detector.js +214 -31
  67. package/src/core/workflows/workflow-router.js +204 -0
  68. package/src/dashboard/app/pages/AgentMap.jsx +32 -33
  69. package/src/dashboard/app/pages/LiveState.jsx +1 -1
  70. package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
  71. package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
  72. package/src/dashboard/app/pages/Workflows.jsx +2 -2
  73. package/src/dashboard/scanner/agents-scanner.js +30 -35
  74. package/src/lib/agents/metrics-aggregator.js +1 -1
  75. package/src/lib/agents/output-collector.js +7 -8
  76. package/src/lib/context/context-builder.js +289 -0
  77. package/src/lib/detectors/claude-config-detector.js +19 -3
  78. package/src/lib/generators/context-generator.js +20 -42
  79. package/src/lib/generators/index.js +1 -0
  80. package/src/lib/generators/mandate-generator.js +512 -0
  81. package/src/lib/generators/metadata-extractor.js +1 -1
  82. package/src/lib/generators/recap-generator.js +3 -4
  83. package/src/lib/installers/mcp-installer.js +300 -92
  84. package/src/lib/phase-chain/phase-validator.js +36 -40
  85. package/src/lib/stack/stack-profile.js +74 -91
  86. package/src/lib/tasks/task-parser.js +1 -1
  87. package/src/lib/tasks/wave-resolver.js +353 -0
  88. package/src/lib/validators/checkpoint-persistence.js +2 -2
  89. package/src/lib/validators/content/cross-phase-validator.js +281 -0
  90. package/src/lib/validators/content/index.js +1 -0
  91. package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
  92. package/src/scripts/global-install.js +2 -27
  93. package/src/utils/agents-installer.js +224 -447
  94. package/src/utils/claude-settings-manager.js +57 -15
  95. package/src/utils/file-copier.js +1 -0
  96. package/src/utils/hooks-installer.js +15 -117
  97. package/src/utils/skills-installer.js +12 -18
  98. package/bin/detect-agents.js +0 -226
  99. package/bin/task-manager.js +0 -1036
  100. package/framework/agents/README.md +0 -13
  101. package/framework/agents/ai-agents/ai-system-architect.md +0 -194
  102. package/framework/agents/ai-agents/vector-search-expert.md +0 -247
  103. package/framework/agents/architecture/architecture-expert.md +0 -98
  104. package/framework/agents/architecture/morph-spec-architect.md +0 -66
  105. package/framework/agents/architecture/packages-validator.md +0 -89
  106. package/framework/agents/architecture/security-expert.md +0 -79
  107. package/framework/agents/architecture/seo-growth-hacker.md +0 -168
  108. package/framework/agents/architecture/standards-architect.md +0 -153
  109. package/framework/agents/backend/dotnet-senior.md +0 -163
  110. package/framework/agents/backend/ef-modeler.md +0 -119
  111. package/framework/agents/backend/hangfire-orchestrator.md +0 -133
  112. package/framework/agents/backend/ms-agent-expert.md +0 -55
  113. package/framework/agents/backend/neon-expert.md +0 -379
  114. package/framework/agents/frontend/css-specialist.md +0 -89
  115. package/framework/agents/frontend/design-system-validator.md +0 -100
  116. package/framework/agents/frontend/gsap-animator.md +0 -131
  117. package/framework/agents/frontend/nextjs-component-validator.md +0 -97
  118. package/framework/agents/frontend/nextjs-expert.md +0 -201
  119. package/framework/agents/frontend/ui-designer.md +0 -259
  120. package/framework/agents/infrastructure/infra-architect.md +0 -215
  121. package/framework/agents/integrations/asaas-financial.md +0 -138
  122. package/framework/agents/integrations/neon-auth.md +0 -151
  123. package/framework/agents/integrations/resend-email.md +0 -127
  124. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
  125. package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
  126. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
  127. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
  128. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
  129. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
  130. package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
  131. package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
  132. package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
  133. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
  134. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
  135. package/framework/hooks/shared/compact-restore.js +0 -123
  136. package/framework/hooks/shared/dispatch-helpers.js +0 -147
  137. package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
  138. package/framework/hooks/shared/stale-task-reset.js +0 -57
  139. package/framework/hooks/shared/worktree-helpers.js +0 -75
  140. package/framework/skills/level-0-meta/README.md +0 -7
  141. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
  142. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
  143. package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
  144. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
  145. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
  146. package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
  147. package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
  148. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
  149. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
  150. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
  151. package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
  152. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
  153. package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
  154. package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
  155. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
  156. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -382
  157. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
  158. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
  159. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
  160. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
  161. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
  162. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
  163. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
  164. package/framework/skills/level-1-workflows/README.md +0 -7
  165. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
  166. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
  167. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
  168. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
  169. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
  170. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  171. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -493
  172. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
  173. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -46
  174. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
  175. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
  176. package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
  177. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
  178. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  179. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
  180. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
  181. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
  182. package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
  183. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -788
  184. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
  185. package/framework/squad-templates/backend-only.json +0 -47
  186. package/framework/squad-templates/frontend-only.json +0 -56
  187. package/framework/squad-templates/full-stack.json +0 -74
  188. package/src/commands/agents/agent-metrics.js +0 -174
  189. package/src/commands/agents/audit-dispatch.js +0 -82
  190. package/src/commands/agents/detect-agents.js +0 -275
  191. package/src/commands/agents/dispatch-agents.js +0 -461
  192. package/src/commands/agents/index.js +0 -13
  193. package/src/commands/agents/manage-agents.js +0 -352
  194. package/src/commands/agents/project-metrics.js +0 -74
  195. package/src/commands/phase/phase-reset.js +0 -74
  196. package/src/commands/scope/escalate.js +0 -217
  197. package/src/commands/state/advance-phase.js +0 -600
  198. package/src/commands/state/phase-runner.js +0 -215
  199. package/src/commands/state/state.js +0 -444
  200. package/src/commands/task/expand.js +0 -100
  201. package/src/commands/tasks/index.js +0 -4
  202. package/src/commands/tasks/task.js +0 -105
  203. package/src/commands/templates/index.js +0 -5
  204. package/src/commands/templates/template-render.js +0 -93
  205. package/src/commands/trust/trust.js +0 -118
  206. package/src/lib/agents/agents-manager.js +0 -349
  207. package/src/lib/build/build-runner.js +0 -99
  208. package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
  209. package/src/lib/contracts/sprint-contract-generator.js +0 -137
  210. package/src/lib/scope/impact-analyzer.js +0 -106
  211. package/src/lib/trust/trust-manager.js +0 -157
  212. package/src/utils/tier-helpers.js +0 -24
  213. /package/framework/hooks/claude-code/{post-tool-use → core}/loop-detect.js +0 -0
@@ -1,376 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * scan-accessibility.mjs
4
- *
5
- * Scans .tsx/.ts (Next.js) and .razor (Blazor) files for static accessibility violations.
6
- * Executed locally by Claude via Bash — output returned to Claude.
7
- *
8
- * Usage:
9
- * node scan-accessibility.mjs [path] # scan directory or file (default: '.')
10
- * node scan-accessibility.mjs src/ # scan src/ recursively
11
- * node scan-accessibility.mjs --summary # counts only, no details
12
- *
13
- * Checks (7 total):
14
- * IMG_MISSING_ALT CRITICAL .tsx/.razor <img without alt= attribute
15
- * INPUT_MISSING_LABEL HIGH .tsx/.razor <input without associated label or aria-label
16
- * <InputText/<FluentTextField without Label=
17
- * HEADING_HIERARCHY HIGH .tsx/.razor Heading level skip (H1→H3 skipping H2)
18
- * LINK_NO_TEXT HIGH .tsx/.razor <a> empty or icon-only without aria-label
19
- * BUTTON_NO_TEXT HIGH .tsx/.razor <button>/<FluentButton> without accessible text
20
- * AUTOPLAY_MEDIA MEDIUM .tsx/.razor <video>/<audio> with autoPlay without muted
21
- * FLUENT_CHECKBOX_NO_LABEL MEDIUM .razor <FluentCheckbox without Label=
22
- *
23
- * Output JSON: { summary: { scanned, findings, bySeverity, clean }, findings: [...] }
24
- * Exit code: 1 if CRITICAL findings, 0 otherwise
25
- */
26
-
27
- import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
28
- import { join, extname, relative } from 'path';
29
-
30
- const targetPath = process.argv[2] ?? '.';
31
- const summaryOnly = process.argv.includes('--summary');
32
-
33
- // ─── File collectors ────────────────────────────────────────────────────────
34
-
35
- const IGNORED_DIRS = ['node_modules', '.git', '.next', 'dist', 'build', '.turbo', 'bin', 'obj'];
36
-
37
- function collectFrontendFiles(dir) {
38
- if (!existsSync(dir)) return [];
39
- const stat = statSync(dir);
40
- if (stat.isFile()) {
41
- const ext = extname(dir);
42
- return ['.tsx', '.ts', '.razor'].includes(ext) ? [dir] : [];
43
- }
44
- const results = [];
45
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
46
- if (entry.isDirectory() && !IGNORED_DIRS.includes(entry.name)) {
47
- results.push(...collectFrontendFiles(join(dir, entry.name)));
48
- } else if (entry.isFile()) {
49
- const ext = extname(entry.name);
50
- if (['.tsx', '.ts', '.razor'].includes(ext)) {
51
- results.push(join(dir, entry.name));
52
- }
53
- }
54
- }
55
- return results;
56
- }
57
-
58
- // ─── Checks ─────────────────────────────────────────────────────────────────
59
-
60
- /**
61
- * Check: IMG_MISSING_ALT — CRITICAL
62
- * Detect <img without alt= in .tsx and .razor files.
63
- * Matches both JSX <img ... /> and HTML <img ...> forms.
64
- */
65
- function checkImgMissingAlt(content, lines, relFile, findings) {
66
- // Match <img tags that do NOT contain alt=
67
- const imgRegex = /<img\b([^>]*?)(?:\/>|>)/g;
68
- let match;
69
- while ((match = imgRegex.exec(content)) !== null) {
70
- const attrs = match[1];
71
- if (!/\balt\s*=/i.test(attrs)) {
72
- const lineNum = content.slice(0, match.index).split('\n').length;
73
- findings.push({
74
- severity: 'CRITICAL',
75
- id: 'IMG_MISSING_ALT',
76
- file: relFile,
77
- line: lineNum,
78
- message: '<img> element missing alt attribute — required for screen readers',
79
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
80
- });
81
- }
82
- }
83
- }
84
-
85
- /**
86
- * Check: INPUT_MISSING_LABEL — HIGH
87
- * Detect <input>, <InputText>, <FluentTextField without accessible label.
88
- * Accepts: aria-label=, aria-labelledby=, or associated <label for=
89
- * For Blazor: Label= attribute on <InputText> and <FluentTextField>
90
- */
91
- function checkInputMissingLabel(content, lines, relFile, findings) {
92
- const isRazor = relFile.endsWith('.razor');
93
-
94
- // --- Standard <input> (both tsx and razor HTML sections) ---
95
- const inputRegex = /<input\b([^>]*?)(?:\/>|>)/g;
96
- let match;
97
- while ((match = inputRegex.exec(content)) !== null) {
98
- const attrs = match[1];
99
- // Skip hidden inputs — they don't need labels
100
- if (/type\s*=\s*['"]hidden['"]/i.test(attrs)) continue;
101
- const hasLabel =
102
- /\baria-label\s*=/i.test(attrs) ||
103
- /\baria-labelledby\s*=/i.test(attrs) ||
104
- /\bid\s*=\s*['"]([^'"]+)['"]/i.test(attrs); // id present → could be associated by <label for>
105
- if (!hasLabel) {
106
- const lineNum = content.slice(0, match.index).split('\n').length;
107
- findings.push({
108
- severity: 'HIGH',
109
- id: 'INPUT_MISSING_LABEL',
110
- file: relFile,
111
- line: lineNum,
112
- message: '<input> element missing accessible label (aria-label, aria-labelledby, or <label for=)',
113
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
114
- });
115
- }
116
- }
117
-
118
- if (isRazor) {
119
- // --- Blazor <InputText> without Label= ---
120
- const inputTextRegex = /<InputText\b([^>]*?)(?:\/>|>)/g;
121
- while ((match = inputTextRegex.exec(content)) !== null) {
122
- const attrs = match[1];
123
- if (!/\bLabel\s*=/i.test(attrs) && !/\baria-label\s*=/i.test(attrs)) {
124
- const lineNum = content.slice(0, match.index).split('\n').length;
125
- findings.push({
126
- severity: 'HIGH',
127
- id: 'INPUT_MISSING_LABEL',
128
- file: relFile,
129
- line: lineNum,
130
- message: '<InputText> component missing Label= attribute (or aria-label)',
131
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
132
- });
133
- }
134
- }
135
-
136
- // --- Fluent UI <FluentTextField> without Label= ---
137
- const fluentTextRegex = /<FluentTextField\b([^>]*?)(?:\/>|>)/g;
138
- while ((match = fluentTextRegex.exec(content)) !== null) {
139
- const attrs = match[1];
140
- if (!/\bLabel\s*=/i.test(attrs) && !/\baria-label\s*=/i.test(attrs)) {
141
- const lineNum = content.slice(0, match.index).split('\n').length;
142
- findings.push({
143
- severity: 'HIGH',
144
- id: 'INPUT_MISSING_LABEL',
145
- file: relFile,
146
- line: lineNum,
147
- message: '<FluentTextField> component missing Label= attribute (or aria-label)',
148
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
149
- });
150
- }
151
- }
152
- }
153
- }
154
-
155
- /**
156
- * Check: HEADING_HIERARCHY — HIGH
157
- * Detect heading level skips (e.g. H1 → H3 without H2) within a file.
158
- * Scans for h1–h6 / H1–H6 tags in order and reports any skip > 1 level.
159
- */
160
- function checkHeadingHierarchy(content, lines, relFile, findings) {
161
- // Collect all headings in document order
162
- const headingRegex = /<[Hh]([1-6])\b/g;
163
- const headings = [];
164
- let match;
165
- while ((match = headingRegex.exec(content)) !== null) {
166
- headings.push({
167
- level: parseInt(match[1], 10),
168
- lineNum: content.slice(0, match.index).split('\n').length,
169
- });
170
- }
171
-
172
- // Check for level skips
173
- for (let i = 1; i < headings.length; i++) {
174
- const prev = headings[i - 1];
175
- const curr = headings[i];
176
- // Only flag increases that skip a level (e.g. h1 → h3)
177
- if (curr.level > prev.level + 1) {
178
- findings.push({
179
- severity: 'HIGH',
180
- id: 'HEADING_HIERARCHY',
181
- file: relFile,
182
- line: curr.lineNum,
183
- message: `Heading level skip: H${prev.level} (line ${prev.lineNum}) → H${curr.level} (line ${curr.lineNum}) — missing H${prev.level + 1}`,
184
- snippet: lines[curr.lineNum - 1]?.trim().slice(0, 100),
185
- });
186
- }
187
- }
188
- }
189
-
190
- /**
191
- * Check: LINK_NO_TEXT — HIGH
192
- * Detect <a> elements that are empty or contain only an icon without aria-label.
193
- * Empty: <a href="..."></a> or <a .../>
194
- * Icon-only: <a href="..."><SomeIcon /></a> or <a href="..."><i class="..."></i></a>
195
- */
196
- function checkLinkNoText(content, lines, relFile, findings) {
197
- // Match opening <a ...> tag and capture attributes, then check for aria-label
198
- // We use a simplified approach: find <a> tags and look for aria-label in attrs
199
- const linkRegex = /<a\b([^>]*?)>([\s\S]*?)<\/a>/g;
200
- let match;
201
- while ((match = linkRegex.exec(content)) !== null) {
202
- const attrs = match[1];
203
- const innerContent = match[2].trim();
204
-
205
- // Has aria-label → OK
206
- if (/\baria-label\s*=/i.test(attrs) || /\baria-labelledby\s*=/i.test(attrs)) continue;
207
-
208
- // Check if inner content has visible text (not just whitespace, tags, or expressions)
209
- // Strip JSX expressions {}, HTML tags, and whitespace
210
- const textOnly = innerContent
211
- .replace(/\{[^}]*\}/g, '') // strip {expressions}
212
- .replace(/<[^>]+>/g, '') // strip HTML/JSX tags
213
- .trim();
214
-
215
- if (!textOnly) {
216
- const lineNum = content.slice(0, match.index).split('\n').length;
217
- findings.push({
218
- severity: 'HIGH',
219
- id: 'LINK_NO_TEXT',
220
- file: relFile,
221
- line: lineNum,
222
- message: '<a> element has no accessible text — add aria-label or visible text content',
223
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
224
- });
225
- }
226
- }
227
- }
228
-
229
- /**
230
- * Check: BUTTON_NO_TEXT — HIGH
231
- * Detect <button> and <FluentButton> elements without accessible text or aria-label.
232
- */
233
- function checkButtonNoText(content, lines, relFile, findings) {
234
- // <button> elements
235
- const buttonRegex = /<button\b([^>]*?)>([\s\S]*?)<\/button>/g;
236
- let match;
237
- while ((match = buttonRegex.exec(content)) !== null) {
238
- const attrs = match[1];
239
- const innerContent = match[2].trim();
240
-
241
- if (/\baria-label\s*=/i.test(attrs) || /\baria-labelledby\s*=/i.test(attrs)) continue;
242
-
243
- const textOnly = innerContent
244
- .replace(/\{[^}]*\}/g, '')
245
- .replace(/<[^>]+>/g, '')
246
- .trim();
247
-
248
- if (!textOnly) {
249
- const lineNum = content.slice(0, match.index).split('\n').length;
250
- findings.push({
251
- severity: 'HIGH',
252
- id: 'BUTTON_NO_TEXT',
253
- file: relFile,
254
- line: lineNum,
255
- message: '<button> element has no accessible text — add aria-label or visible text content',
256
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
257
- });
258
- }
259
- }
260
-
261
- // <FluentButton> (Blazor Fluent UI)
262
- const fluentBtnRegex = /<FluentButton\b([^>]*?)>([\s\S]*?)<\/FluentButton>/g;
263
- while ((match = fluentBtnRegex.exec(content)) !== null) {
264
- const attrs = match[1];
265
- const innerContent = match[2].trim();
266
-
267
- if (/\baria-label\s*=/i.test(attrs) || /\bTitle\s*=/i.test(attrs)) continue;
268
-
269
- const textOnly = innerContent
270
- .replace(/\{[^}]*\}/g, '')
271
- .replace(/<[^>]+>/g, '')
272
- .trim();
273
-
274
- if (!textOnly) {
275
- const lineNum = content.slice(0, match.index).split('\n').length;
276
- findings.push({
277
- severity: 'HIGH',
278
- id: 'BUTTON_NO_TEXT',
279
- file: relFile,
280
- line: lineNum,
281
- message: '<FluentButton> has no accessible text — add aria-label, Title=, or visible text',
282
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
283
- });
284
- }
285
- }
286
- }
287
-
288
- /**
289
- * Check: AUTOPLAY_MEDIA — MEDIUM
290
- * Detect <video> or <audio> with autoPlay attribute but without muted.
291
- * Autoplay without muted violates WCAG 1.4.2 (Audio Control).
292
- */
293
- function checkAutoplayMedia(content, lines, relFile, findings) {
294
- const mediaRegex = /<(video|audio)\b([^>]*?)(?:\/>|>)/gi;
295
- let match;
296
- while ((match = mediaRegex.exec(content)) !== null) {
297
- const attrs = match[2];
298
- // JSX: autoPlay (camelCase), HTML: autoplay
299
- if (/\bautoPlay\b|\bautoplay\b/i.test(attrs) && !/\bmuted\b/i.test(attrs)) {
300
- const lineNum = content.slice(0, match.index).split('\n').length;
301
- findings.push({
302
- severity: 'MEDIUM',
303
- id: 'AUTOPLAY_MEDIA',
304
- file: relFile,
305
- line: lineNum,
306
- message: `<${match[1]}> has autoPlay without muted — violates WCAG 1.4.2 (Audio Control)`,
307
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
308
- });
309
- }
310
- }
311
- }
312
-
313
- /**
314
- * Check: FLUENT_CHECKBOX_NO_LABEL — MEDIUM
315
- * Detect <FluentCheckbox without Label= in .razor files.
316
- */
317
- function checkFluentCheckboxNoLabel(content, lines, relFile, findings) {
318
- if (!relFile.endsWith('.razor')) return;
319
-
320
- const checkboxRegex = /<FluentCheckbox\b([^>]*?)(?:\/>|>)/g;
321
- let match;
322
- while ((match = checkboxRegex.exec(content)) !== null) {
323
- const attrs = match[1];
324
- if (!/\bLabel\s*=/i.test(attrs) && !/\baria-label\s*=/i.test(attrs)) {
325
- const lineNum = content.slice(0, match.index).split('\n').length;
326
- findings.push({
327
- severity: 'MEDIUM',
328
- id: 'FLUENT_CHECKBOX_NO_LABEL',
329
- file: relFile,
330
- line: lineNum,
331
- message: '<FluentCheckbox> missing Label= attribute — screen readers cannot identify this checkbox',
332
- snippet: lines[lineNum - 1]?.trim().slice(0, 100),
333
- });
334
- }
335
- }
336
- }
337
-
338
- // ─── Main ────────────────────────────────────────────────────────────────────
339
-
340
- const files = collectFrontendFiles(targetPath);
341
- const findings = [];
342
-
343
- for (const file of files) {
344
- const content = readFileSync(file, 'utf-8');
345
- const lines = content.split('\n');
346
- const relFile = relative(process.cwd(), file).replace(/\\/g, '/');
347
-
348
- checkImgMissingAlt(content, lines, relFile, findings);
349
- checkInputMissingLabel(content, lines, relFile, findings);
350
- checkHeadingHierarchy(content, lines, relFile, findings);
351
- checkLinkNoText(content, lines, relFile, findings);
352
- checkButtonNoText(content, lines, relFile, findings);
353
- checkAutoplayMedia(content, lines, relFile, findings);
354
- checkFluentCheckboxNoLabel(content, lines, relFile, findings);
355
- }
356
-
357
- const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
358
- for (const f of findings) {
359
- bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
360
- }
361
-
362
- const summary = {
363
- scanned: files.length,
364
- findings: findings.length,
365
- bySeverity,
366
- clean: findings.length === 0,
367
- };
368
-
369
- if (summaryOnly) {
370
- console.log(JSON.stringify(summary, null, 2));
371
- } else {
372
- console.log(JSON.stringify({ summary, findings }, null, 2));
373
- }
374
-
375
- // Exit 1 if any CRITICAL findings
376
- process.exit(findings.some(f => f.severity === 'CRITICAL') ? 1 : 0);