@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.
- package/CLAUDE.md +57 -71
- package/README.md +2 -2
- package/bin/morph-spec.js +224 -491
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +55 -38
- package/framework/MORPH.md +244 -0
- package/framework/agents.json +127 -1183
- package/framework/commands/morph-apply.md +56 -240
- package/framework/commands/morph-archive.md +48 -85
- package/framework/commands/morph-preflight.md +7 -7
- package/framework/commands/morph-proposal.md +35 -191
- package/framework/commands/morph-status.md +19 -50
- package/framework/commands/morph-troubleshoot.md +2 -2
- package/framework/evals/rubrics/README.md +44 -0
- package/framework/evals/rubrics/architecture.md +46 -0
- package/framework/evals/rubrics/code-quality.md +50 -0
- package/framework/evals/rubrics/contracts.md +47 -0
- package/framework/evals/rubrics/test-coverage.md +50 -0
- package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
- package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
- package/framework/evals/suites/standard.yaml +5 -0
- package/framework/evals/trace-schema.md +67 -0
- package/framework/hooks/claude-code/core/README.md +3 -0
- package/framework/hooks/claude-code/core/denylist.json +14 -0
- package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
- package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
- package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
- package/framework/hooks/shared/phase-utils.js +20 -28
- package/framework/hooks/shared/state-reader.js +4 -5
- package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -210
- package/framework/phases.json +40 -114
- package/framework/rubrics/eval-rubric.json +66 -0
- package/framework/skills/README.md +39 -64
- package/framework/skills/core/README.md +3 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
- package/framework/skills/core/morph-eval/SKILL.md +57 -0
- package/framework/skills/core/morph-implement/SKILL.md +83 -0
- package/framework/skills/core/morph-plan/SKILL.md +64 -0
- package/framework/skills/core/morph-review/SKILL.md +62 -0
- package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
- package/framework/skills/core/morph-standards/SKILL.md +49 -0
- package/framework/standards/STANDARDS.json +32 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -0
- package/framework/templates/CLAUDE.md.core.guide.md +29 -0
- package/framework/templates/CLAUDE.md.core.template +22 -0
- package/framework/templates/REGISTRY.json +4 -4
- package/framework/workflows/configs/fast-track.json +5 -10
- package/framework/workflows/configs/full.json +4 -8
- package/framework/workflows/configs/quick.json +32 -0
- package/framework/workflows/configs/standard.json +57 -64
- package/package.json +7 -6
- package/src/commands/mcp/mcp-setup.js +58 -31
- package/src/commands/project/doctor.js +69 -70
- package/src/commands/project/init.js +26 -5
- package/src/commands/project/status.js +18 -19
- package/src/commands/state/approve.js +1 -1
- package/src/commands/validation/validate-feature.js +18 -112
- package/src/core/paths/output-schema.js +133 -151
- package/src/core/state/phase-state-machine.js +10 -24
- package/src/core/state/state-manager.js +17 -17
- package/src/core/workflows/workflow-detector.js +214 -31
- package/src/core/workflows/workflow-router.js +204 -0
- package/src/dashboard/app/pages/AgentMap.jsx +32 -33
- package/src/dashboard/app/pages/LiveState.jsx +1 -1
- package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
- package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
- package/src/dashboard/app/pages/Workflows.jsx +2 -2
- package/src/dashboard/scanner/agents-scanner.js +30 -35
- package/src/lib/agents/metrics-aggregator.js +1 -1
- package/src/lib/agents/output-collector.js +7 -8
- package/src/lib/context/context-builder.js +289 -0
- package/src/lib/detectors/claude-config-detector.js +19 -3
- package/src/lib/generators/context-generator.js +20 -42
- package/src/lib/generators/index.js +1 -0
- package/src/lib/generators/mandate-generator.js +512 -0
- package/src/lib/generators/metadata-extractor.js +1 -1
- package/src/lib/generators/recap-generator.js +3 -4
- package/src/lib/installers/mcp-installer.js +300 -92
- package/src/lib/phase-chain/phase-validator.js +36 -40
- package/src/lib/stack/stack-profile.js +74 -91
- package/src/lib/tasks/task-parser.js +1 -1
- package/src/lib/tasks/wave-resolver.js +353 -0
- package/src/lib/validators/checkpoint-persistence.js +2 -2
- package/src/lib/validators/content/cross-phase-validator.js +281 -0
- package/src/lib/validators/content/index.js +1 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
- package/src/scripts/global-install.js +2 -27
- package/src/utils/agents-installer.js +224 -447
- package/src/utils/claude-settings-manager.js +57 -15
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +15 -117
- package/src/utils/skills-installer.js +12 -18
- package/bin/detect-agents.js +0 -226
- package/bin/task-manager.js +0 -1036
- package/framework/agents/README.md +0 -13
- package/framework/agents/ai-agents/ai-system-architect.md +0 -194
- package/framework/agents/ai-agents/vector-search-expert.md +0 -247
- package/framework/agents/architecture/architecture-expert.md +0 -98
- package/framework/agents/architecture/morph-spec-architect.md +0 -66
- package/framework/agents/architecture/packages-validator.md +0 -89
- package/framework/agents/architecture/security-expert.md +0 -79
- package/framework/agents/architecture/seo-growth-hacker.md +0 -168
- package/framework/agents/architecture/standards-architect.md +0 -153
- package/framework/agents/backend/dotnet-senior.md +0 -163
- package/framework/agents/backend/ef-modeler.md +0 -119
- package/framework/agents/backend/hangfire-orchestrator.md +0 -133
- package/framework/agents/backend/ms-agent-expert.md +0 -55
- package/framework/agents/backend/neon-expert.md +0 -379
- package/framework/agents/frontend/css-specialist.md +0 -89
- package/framework/agents/frontend/design-system-validator.md +0 -100
- package/framework/agents/frontend/gsap-animator.md +0 -131
- package/framework/agents/frontend/nextjs-component-validator.md +0 -97
- package/framework/agents/frontend/nextjs-expert.md +0 -201
- package/framework/agents/frontend/ui-designer.md +0 -259
- package/framework/agents/infrastructure/infra-architect.md +0 -215
- package/framework/agents/integrations/asaas-financial.md +0 -138
- package/framework/agents/integrations/neon-auth.md +0 -151
- package/framework/agents/integrations/resend-email.md +0 -127
- package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
- package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
- package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
- package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
- package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
- package/framework/hooks/shared/compact-restore.js +0 -123
- package/framework/hooks/shared/dispatch-helpers.js +0 -147
- package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
- package/framework/hooks/shared/stale-task-reset.js +0 -57
- package/framework/hooks/shared/worktree-helpers.js +0 -75
- package/framework/skills/level-0-meta/README.md +0 -7
- package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
- package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
- package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
- package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
- package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
- package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
- package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
- package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
- package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
- package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
- package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -382
- package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
- package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
- package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
- package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
- package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
- package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
- package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
- package/framework/skills/level-1-workflows/README.md +0 -7
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
- package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
- package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
- package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -493
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -46
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
- package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
- package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
- package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
- package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
- package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -788
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
- package/framework/squad-templates/backend-only.json +0 -47
- package/framework/squad-templates/frontend-only.json +0 -56
- package/framework/squad-templates/full-stack.json +0 -74
- package/src/commands/agents/agent-metrics.js +0 -174
- package/src/commands/agents/audit-dispatch.js +0 -82
- package/src/commands/agents/detect-agents.js +0 -275
- package/src/commands/agents/dispatch-agents.js +0 -461
- package/src/commands/agents/index.js +0 -13
- package/src/commands/agents/manage-agents.js +0 -352
- package/src/commands/agents/project-metrics.js +0 -74
- package/src/commands/phase/phase-reset.js +0 -74
- package/src/commands/scope/escalate.js +0 -217
- package/src/commands/state/advance-phase.js +0 -600
- package/src/commands/state/phase-runner.js +0 -215
- package/src/commands/state/state.js +0 -444
- package/src/commands/task/expand.js +0 -100
- package/src/commands/tasks/index.js +0 -4
- package/src/commands/tasks/task.js +0 -105
- package/src/commands/templates/index.js +0 -5
- package/src/commands/templates/template-render.js +0 -93
- package/src/commands/trust/trust.js +0 -118
- package/src/lib/agents/agents-manager.js +0 -349
- package/src/lib/build/build-runner.js +0 -99
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
- package/src/lib/contracts/sprint-contract-generator.js +0 -137
- package/src/lib/scope/impact-analyzer.js +0 -106
- package/src/lib/trust/trust-manager.js +0 -157
- package/src/utils/tier-helpers.js +0 -24
- /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);
|