freeharness 0.4.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/LICENSE +21 -0
- package/NOTICE +108 -0
- package/README.md +134 -0
- package/canonical/HARVEST.yaml +858 -0
- package/canonical/PROVENANCE.yaml +740 -0
- package/canonical/agents/README.md +95 -0
- package/canonical/agents/agentic-architect.md +18 -0
- package/canonical/agents/database-reviewer.md +41 -0
- package/canonical/agents/explorer-ai-engineer-analyst.md +65 -0
- package/canonical/agents/pr-reviewer.md +96 -0
- package/canonical/agents/python-reviewer.md +55 -0
- package/canonical/agents/report-formatter.md +19 -0
- package/canonical/agents/security-reviewer.md +53 -0
- package/canonical/agents/test-writer.md +34 -0
- package/canonical/commands/fh-context.md +37 -0
- package/canonical/evals/promptfoo.redteam.yaml +108 -0
- package/canonical/hooks/cost-prices.json +14 -0
- package/canonical/hooks/freeharness-auto-memory.mjs +158 -0
- package/canonical/hooks/freeharness-config-guard.mjs +162 -0
- package/canonical/hooks/freeharness-context-monitor.mjs +198 -0
- package/canonical/hooks/freeharness-converge.mjs +227 -0
- package/canonical/hooks/freeharness-cost-tracker.mjs +1027 -0
- package/canonical/hooks/freeharness-memory-guard.mjs +57 -0
- package/canonical/hooks/freeharness-output-pruner.mjs +333 -0
- package/canonical/hooks/freeharness-precompact.mjs +323 -0
- package/canonical/hooks/freeharness-repeat-tool.mjs +208 -0
- package/canonical/hooks/freeharness-security-guidance.mjs +164 -0
- package/canonical/hooks/freeharness-session-start +583 -0
- package/canonical/hooks/hooks.auto-memory.claude-code.json +16 -0
- package/canonical/hooks/hooks.claude-code.json +17 -0
- package/canonical/hooks/hooks.config-guard.claude-code.json +16 -0
- package/canonical/hooks/hooks.context-monitor.claude-code.json +15 -0
- package/canonical/hooks/hooks.converge.claude-code.json +15 -0
- package/canonical/hooks/hooks.cost-tracker.claude-code.json +37 -0
- package/canonical/hooks/hooks.memory-guard.claude-code.json +16 -0
- package/canonical/hooks/hooks.output-pruner.claude-code.json +16 -0
- package/canonical/hooks/hooks.precompact.claude-code.json +15 -0
- package/canonical/hooks/hooks.repeat-tool.claude-code.json +25 -0
- package/canonical/hooks/hooks.security-guidance.claude-code.json +16 -0
- package/canonical/mcp/servers.json +20 -0
- package/canonical/openspec/config.base.yaml +89 -0
- package/canonical/openspec/schema.spec-driven.yaml +277 -0
- package/canonical/openspec/templates/data-contract.md +36 -0
- package/canonical/openspec/templates/design.md +84 -0
- package/canonical/openspec/templates/eval-plan.md +33 -0
- package/canonical/openspec/templates/follow-ups.md +2 -0
- package/canonical/openspec/templates/proposal.md +45 -0
- package/canonical/openspec/templates/spec.md +42 -0
- package/canonical/openspec/templates/tasks.md +38 -0
- package/canonical/profiles/local-llamacpp.LOCAL-MODELS.md +47 -0
- package/canonical/profiles/local-llamacpp.json +27 -0
- package/canonical/profiles/local-ollama.json +20 -0
- package/canonical/profiles/mcp.json +91 -0
- package/canonical/rules/CLAUDE.md +78 -0
- package/canonical/rules/project-CLAUDE.template.md +62 -0
- package/canonical/rules/routing.md +20 -0
- package/canonical/rules/safety.md +41 -0
- package/canonical/settings/settings.cache-hygiene.claude-code.json +5 -0
- package/canonical/skills/agent-evaluation/SKILL.md +102 -0
- package/canonical/skills/agent-evaluation/assets/evaluation_report_template.md +206 -0
- package/canonical/skills/agent-evaluation/evals/cases.yaml +106 -0
- package/canonical/skills/agent-evaluation/references/dataset-preparation.md +351 -0
- package/canonical/skills/agent-evaluation/references/scorers-constraints.md +165 -0
- package/canonical/skills/agent-evaluation/references/scorers.md +431 -0
- package/canonical/skills/agent-evaluation/references/setup-guide.md +252 -0
- package/canonical/skills/agent-evaluation/references/tracing-integration.md +164 -0
- package/canonical/skills/agent-evaluation/references/troubleshooting.md +514 -0
- package/canonical/skills/agent-evaluation/scripts/analyze_results.py +462 -0
- package/canonical/skills/agent-evaluation/scripts/create_dataset_template.py +382 -0
- package/canonical/skills/agent-evaluation/scripts/list_datasets.py +273 -0
- package/canonical/skills/agent-evaluation/scripts/run_evaluation_template.py +309 -0
- package/canonical/skills/agent-evaluation/scripts/setup_mlflow.py +353 -0
- package/canonical/skills/agent-evaluation/scripts/utils/__init__.py +27 -0
- package/canonical/skills/agent-evaluation/scripts/utils/env_validation.py +123 -0
- package/canonical/skills/agent-evaluation/scripts/utils/tracing_utils.py +108 -0
- package/canonical/skills/agent-evaluation/scripts/validate_agent_tracing.py +148 -0
- package/canonical/skills/agent-evaluation/scripts/validate_auth.py +240 -0
- package/canonical/skills/agent-evaluation/scripts/validate_environment.py +185 -0
- package/canonical/skills/agent-evaluation/scripts/validate_tracing_runtime.py +318 -0
- package/canonical/skills/agentic-architecture/SKILL.md +41 -0
- package/canonical/skills/agentic-architecture/evals/cases.yaml +94 -0
- package/canonical/skills/agentic-architecture/reference/context-engineering.md +75 -0
- package/canonical/skills/agentic-architecture/reference/patterns.md +76 -0
- package/canonical/skills/agentic-architecture/reference/reliability-observability.md +94 -0
- package/canonical/skills/agentic-architecture/reference/review-framework.md +59 -0
- package/canonical/skills/code-review/SKILL.md +52 -0
- package/canonical/skills/code-review/evals/cases.yaml +197 -0
- package/canonical/skills/git-workflow/SKILL.md +41 -0
- package/canonical/skills/git-workflow/evals/cases.yaml +92 -0
- package/canonical/skills/local-code/SKILL.md +24 -0
- package/canonical/skills/local-code/evals/cases.yaml +85 -0
- package/canonical/skills/local-code/scripts/query_llama.py +68 -0
- package/canonical/skills/report-formatting/SKILL.md +62 -0
- package/canonical/skills/report-formatting/evals/cases.yaml +83 -0
- package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +135 -0
- package/canonical/skills/report-formatting/reference/HELPERS.md +120 -0
- package/canonical/skills/report-formatting/reference/PPTX.md +136 -0
- package/canonical/skills/report-formatting/reference/RELEASE.md +99 -0
- package/canonical/skills/report-formatting/reference/TEMPLATES.md +48 -0
- package/canonical/skills/report-formatting/reference/authoring.md +66 -0
- package/canonical/skills/report-formatting/scripts/architecture-template.md +271 -0
- package/canonical/skills/report-formatting/scripts/assets/claro-logo.png +0 -0
- package/canonical/skills/report-formatting/scripts/assets/claro-wordmark.svg +1 -0
- package/canonical/skills/report-formatting/scripts/build-architecture.js +57 -0
- package/canonical/skills/report-formatting/scripts/build-md-claro.js +224 -0
- package/canonical/skills/report-formatting/scripts/build-release.js +51 -0
- package/canonical/skills/report-formatting/scripts/check-env.js +58 -0
- package/canonical/skills/report-formatting/scripts/docmodel.js +283 -0
- package/canonical/skills/report-formatting/scripts/example-build.js +119 -0
- package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +195 -0
- package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +114 -0
- package/canonical/skills/report-formatting/scripts/package.json +17 -0
- package/canonical/skills/report-formatting/scripts/pptx/example_build_StatusReport_pptx.py +223 -0
- package/canonical/skills/report-formatting/scripts/pptx/example_build_WeeklyStatus_pptx.py +245 -0
- package/canonical/skills/report-formatting/scripts/pptx/pptx_office.py +144 -0
- package/canonical/skills/report-formatting/scripts/pptx/requirements.txt +7 -0
- package/canonical/skills/report-formatting/scripts/pptx/status_report_blocks.py +193 -0
- package/canonical/skills/report-formatting/scripts/pptx/theme_claro_pptx.py +473 -0
- package/canonical/skills/report-formatting/scripts/release-template.md +80 -0
- package/canonical/skills/report-formatting/scripts/render-mermaid.js +105 -0
- package/canonical/skills/report-formatting/scripts/theme-claro.js +403 -0
- package/canonical/skills/subagent-driven-development/SKILL.md +60 -0
- package/canonical/skills/subagent-driven-development/evals/cases.yaml +208 -0
- package/canonical/skills/subagent-driven-development/reference/brief.md +91 -0
- package/canonical/skills/subagent-driven-development/reference/handoff.md +118 -0
- package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +850 -0
- package/canonical/skills/systematic-debugging/SKILL.md +44 -0
- package/canonical/skills/systematic-debugging/evals/cases.yaml +90 -0
- package/canonical/skills/test-driven-development/SKILL.md +42 -0
- package/canonical/skills/test-driven-development/evals/cases.yaml +86 -0
- package/canonical/skills/test-driven-development/reference/writing-good-tests.md +52 -0
- package/canonical/skills/verification-before-completion/SKILL.md +53 -0
- package/canonical/skills/verification-before-completion/evals/cases.yaml +107 -0
- package/dist/adapters/content/language-lint.d.ts +46 -0
- package/dist/adapters/content/language-lint.js +191 -0
- package/dist/adapters/harness/claude-code.d.ts +57 -0
- package/dist/adapters/harness/claude-code.js +385 -0
- package/dist/adapters/harness/index.d.ts +3 -0
- package/dist/adapters/harness/index.js +17 -0
- package/dist/adapters/harness/opencode.d.ts +7 -0
- package/dist/adapters/harness/opencode.js +166 -0
- package/dist/adapters/harness/plugin.d.ts +22 -0
- package/dist/adapters/harness/plugin.js +257 -0
- package/dist/adapters/memory/doctor.d.ts +480 -0
- package/dist/adapters/memory/doctor.js +1217 -0
- package/dist/adapters/memory/vendored.d.ts +150 -0
- package/dist/adapters/memory/vendored.js +487 -0
- package/dist/adapters/package/node.d.ts +9 -0
- package/dist/adapters/package/node.js +20 -0
- package/dist/adapters/prompt/readline.d.ts +19 -0
- package/dist/adapters/prompt/readline.js +53 -0
- package/dist/adapters/sync/diff.d.ts +43 -0
- package/dist/adapters/sync/diff.js +380 -0
- package/dist/cli/cleanup.d.ts +86 -0
- package/dist/cli/cleanup.js +382 -0
- package/dist/cli/cost.d.ts +304 -0
- package/dist/cli/cost.js +501 -0
- package/dist/cli/doctor.d.ts +205 -0
- package/dist/cli/doctor.js +654 -0
- package/dist/cli/executor.d.ts +5 -0
- package/dist/cli/executor.js +48 -0
- package/dist/cli/plugin-validate.d.ts +37 -0
- package/dist/cli/plugin-validate.js +256 -0
- package/dist/cli/prefix.d.ts +68 -0
- package/dist/cli/prefix.js +218 -0
- package/dist/cli/profile.d.ts +170 -0
- package/dist/cli/profile.js +285 -0
- package/dist/cli/runner.d.ts +10 -0
- package/dist/cli/runner.js +27 -0
- package/dist/cli/uninstall.d.ts +65 -0
- package/dist/cli/uninstall.js +147 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +989 -0
- package/dist/infra/dogfood.d.ts +31 -0
- package/dist/infra/dogfood.js +51 -0
- package/dist/infra/fsutil.d.ts +40 -0
- package/dist/infra/fsutil.js +143 -0
- package/dist/infra/manifest.d.ts +76 -0
- package/dist/infra/manifest.js +92 -0
- package/dist/infra/paths.d.ts +19 -0
- package/dist/infra/paths.js +74 -0
- package/dist/infra/routing.d.ts +4 -0
- package/dist/infra/routing.js +7 -0
- package/node_modules/@freeharness/core/dist/context.d.ts +30 -0
- package/node_modules/@freeharness/core/dist/context.js +1 -0
- package/node_modules/@freeharness/core/dist/index.d.ts +5 -0
- package/node_modules/@freeharness/core/dist/index.js +6 -0
- package/node_modules/@freeharness/core/dist/planner.d.ts +13 -0
- package/node_modules/@freeharness/core/dist/planner.js +18 -0
- package/node_modules/@freeharness/core/dist/ports.d.ts +90 -0
- package/node_modules/@freeharness/core/dist/ports.js +1 -0
- package/node_modules/@freeharness/core/dist/registry.d.ts +41 -0
- package/node_modules/@freeharness/core/dist/registry.js +124 -0
- package/node_modules/@freeharness/core/dist/tiers.d.ts +30 -0
- package/node_modules/@freeharness/core/dist/tiers.js +99 -0
- package/node_modules/@freeharness/core/package.json +34 -0
- package/node_modules/@freeharness/sdd/PROVENANCE.yaml +175 -0
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.d.ts +2 -0
- package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +166 -0
- package/node_modules/@freeharness/sdd/dist/src/index.d.ts +14 -0
- package/node_modules/@freeharness/sdd/dist/src/index.js +11 -0
- package/node_modules/@freeharness/sdd/dist/src/init.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/src/init.js +190 -0
- package/node_modules/@freeharness/sdd/dist/src/root-resolver.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/src/root-resolver.js +32 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +81 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +483 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/context.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/context.js +48 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +57 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +279 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +67 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +460 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +5 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/list.js +95 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +150 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/schemas.d.ts +4 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/schemas.js +23 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +56 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +140 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/status.js +85 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +6 -0
- package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +229 -0
- package/node_modules/@freeharness/sdd/dist/src/validate-change.d.ts +40 -0
- package/node_modules/@freeharness/sdd/dist/src/validate-change.js +114 -0
- package/node_modules/@freeharness/sdd/dist/src/validate.d.ts +24 -0
- package/node_modules/@freeharness/sdd/dist/src/validate.js +72 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.d.ts +130 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +981 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-metadata.d.ts +46 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-metadata.js +128 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +26 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/file-system.d.ts +50 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/file-system.js +394 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/global-config.d.ts +15 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/global-config.js +49 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/graph.d.ts +71 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/graph.js +164 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +21 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/instruction-loader.d.ts +102 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/instruction-loader.js +230 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +18 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +138 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/resolver.d.ts +82 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/resolver.js +333 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/schema.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/schema.js +110 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/state.d.ts +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/state.js +32 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.d.ts +33 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +149 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +49 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/claude.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/claude.js +31 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/index.d.ts +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/index.js +10 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/opencode.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/adapters/opencode.js +26 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/generator.d.ts +28 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/generator.js +41 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/index.d.ts +23 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/index.js +25 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/invocation.d.ts +72 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/invocation.js +76 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/registry.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/registry.js +53 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/types.d.ts +62 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/types.js +7 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/yaml.d.ts +35 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-generation/yaml.js +47 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-references.d.ts +33 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/command-references.js +55 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/allowed-tools.d.ts +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/allowed-tools.js +11 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/skill-generation.d.ts +48 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/shared/skill-generation.js +99 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/skill-templates.d.ts +19 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/skill-templates.js +18 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/types.d.ts +18 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/types.js +4 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.d.ts +19 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +207 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +409 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/bulk-archive-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/bulk-archive-change.js +673 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/continue-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/continue-change.js +230 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +550 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/feedback.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/feedback.js +107 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/ff-change.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/ff-change.js +228 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/new-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/new-change.js +147 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/onboard.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/onboard.js +571 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +322 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/store-selection.d.ts +7 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/store-selection.js +7 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +518 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +176 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/verify-change.d.ts +9 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/verify-change.js +344 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +54 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/delta-resolution.d.ts +35 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/delta-resolution.js +171 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +27 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +210 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/code-fence.d.ts +14 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/code-fence.js +48 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.d.ts +22 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +186 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +65 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +409 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +55 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +98 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.d.ts +8 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +77 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.d.ts +44 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +116 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/base.schema.d.ts +12 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/base.schema.js +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/change.schema.d.ts +72 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/change.schema.js +30 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/index.d.ts +3 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/index.js +3 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/spec.schema.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/schemas/spec.schema.js +14 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +39 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +114 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +45 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +105 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-numbering.d.ts +15 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-numbering.js +56 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/types.d.ts +17 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/types.js +1 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +94 -0
- package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +756 -0
- package/node_modules/@freeharness/sdd/package.json +57 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/LICENSE.upstream +22 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +232 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/README.md +19 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +187 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +181 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +338 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +117 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +342 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +116 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +76 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +560 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +162 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +261 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +90 -0
- package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +174 -0
- package/package.json +70 -0
- package/provenance/canonical.PROVENANCE.yaml +740 -0
- package/provenance/claude-mem.LICENSE.upstream +202 -0
- package/provenance/memory.PROVENANCE.yaml +399 -0
- package/provenance/sdd.PROVENANCE.yaml +175 -0
- package/vendor-licenses/caveman.LICENSE +27 -0
- package/vendor-licenses/deepseek-harness.LICENSE +21 -0
- package/vendor-licenses/ecc.LICENSE +21 -0
- package/vendor-licenses/pi.LICENSE +21 -0
- package/vendor-licenses/superpowers.LICENSE +21 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { readFileSync, promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { SpecSchema, ChangeSchema } from './schemas/index.js';
|
|
4
|
+
import { MarkdownParser } from './parsers/markdown-parser.js';
|
|
5
|
+
import { ChangeParser } from './parsers/change-parser.js';
|
|
6
|
+
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
|
|
7
|
+
import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, findMissingCurrentScenarios, } from './parsers/requirement-blocks.js';
|
|
8
|
+
import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, } from './parsers/requirement-text.js';
|
|
9
|
+
import { findMainSpecStructureIssues } from './parsers/spec-structure.js';
|
|
10
|
+
import { FileSystemUtils } from '../engine/file-system.js';
|
|
11
|
+
import { discoverSpecFiles } from './spec-discovery.js';
|
|
12
|
+
import { findTaskNumberingIssues } from './task-numbering.js';
|
|
13
|
+
import { findPurposePlaceholderIssue } from './purpose-placeholder.js';
|
|
14
|
+
import { findSpecUpdates, buildUpdatedSpec } from '../apply/specs-apply.js';
|
|
15
|
+
import { findUnresolvedDeltaHeaders } from './delta-resolution.js';
|
|
16
|
+
export class Validator {
|
|
17
|
+
strictMode;
|
|
18
|
+
constructor(strictMode = false) {
|
|
19
|
+
this.strictMode = strictMode;
|
|
20
|
+
}
|
|
21
|
+
async validateSpec(filePath) {
|
|
22
|
+
const issues = [];
|
|
23
|
+
const specName = this.extractNameFromPath(filePath);
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
26
|
+
const parser = new MarkdownParser(content);
|
|
27
|
+
const spec = parser.parseSpec(specName);
|
|
28
|
+
const result = SpecSchema.safeParse(spec);
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
issues.push(...this.convertZodErrors(result.error));
|
|
31
|
+
}
|
|
32
|
+
issues.push(...this.applySpecRules(spec, content));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
36
|
+
const enriched = this.enrichTopLevelError(specName, baseMessage);
|
|
37
|
+
issues.push({
|
|
38
|
+
level: 'ERROR',
|
|
39
|
+
path: 'file',
|
|
40
|
+
message: enriched,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return this.createReport(issues);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate spec content from a string (used for pre-write validation of rebuilt specs)
|
|
47
|
+
*/
|
|
48
|
+
async validateSpecContent(specName, content) {
|
|
49
|
+
const issues = [];
|
|
50
|
+
try {
|
|
51
|
+
const parser = new MarkdownParser(content);
|
|
52
|
+
const spec = parser.parseSpec(specName);
|
|
53
|
+
const result = SpecSchema.safeParse(spec);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
issues.push(...this.convertZodErrors(result.error));
|
|
56
|
+
}
|
|
57
|
+
issues.push(...this.applySpecRules(spec, content));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
61
|
+
const enriched = this.enrichTopLevelError(specName, baseMessage);
|
|
62
|
+
issues.push({ level: 'ERROR', path: 'file', message: enriched });
|
|
63
|
+
}
|
|
64
|
+
return this.createReport(issues);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate the change proposal document (Why / What Changes prose + the
|
|
68
|
+
* top-level ChangeSchema). Delta-level checks live in
|
|
69
|
+
* `validateChangeDeltaSpecs`. This is the non-blocking proposal-level pass
|
|
70
|
+
* archive uses; TRIMMED here to drop the skip_specs marker awareness.
|
|
71
|
+
*/
|
|
72
|
+
async validateChange(filePath) {
|
|
73
|
+
const issues = [];
|
|
74
|
+
const changeName = this.extractNameFromPath(filePath);
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
77
|
+
const changeDir = path.dirname(filePath);
|
|
78
|
+
const parser = new ChangeParser(content, changeDir);
|
|
79
|
+
const change = await parser.parseChangeWithDeltas(changeName);
|
|
80
|
+
const result = ChangeSchema.safeParse(change);
|
|
81
|
+
if (!result.success) {
|
|
82
|
+
issues.push(...this.convertZodErrors(result.error));
|
|
83
|
+
}
|
|
84
|
+
issues.push(...this.applyChangeRules(change, content));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
88
|
+
const enriched = this.enrichTopLevelError(changeName, baseMessage);
|
|
89
|
+
issues.push({
|
|
90
|
+
level: 'ERROR',
|
|
91
|
+
path: 'file',
|
|
92
|
+
message: enriched,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return this.createReport(issues);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validate delta-formatted spec files under a change directory.
|
|
99
|
+
* Enforces:
|
|
100
|
+
* - At least one delta across all files
|
|
101
|
+
* - ADDED/MODIFIED: each requirement has at least one scenario; missing
|
|
102
|
+
* English SHALL/MUST keywords are guidance unless strict mode is enabled
|
|
103
|
+
* - REMOVED: names only; no scenario/description required
|
|
104
|
+
* - RENAMED: pairs well-formed
|
|
105
|
+
* - No duplicates within sections; no cross-section conflicts per spec
|
|
106
|
+
*
|
|
107
|
+
* When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
|
|
108
|
+
* against the current main specs for the scenario loss archive refuses to
|
|
109
|
+
* apply (#1477). When `options.projectRoot` is given, the change's tasks.md is
|
|
110
|
+
* checked for ambiguous task numbering (#1520).
|
|
111
|
+
*/
|
|
112
|
+
async validateChangeDeltaSpecs(changeDir, options = {}) {
|
|
113
|
+
const issues = [];
|
|
114
|
+
const specsDir = path.join(changeDir, 'specs');
|
|
115
|
+
let totalDeltas = 0;
|
|
116
|
+
let hasRootLevelSpec = false;
|
|
117
|
+
const missingHeaderSpecs = [];
|
|
118
|
+
const emptySectionSpecs = [];
|
|
119
|
+
try {
|
|
120
|
+
// Discover delta specs through the same helper the change parser, show,
|
|
121
|
+
// apply, and archive use, so validate never accepts a layout the merge
|
|
122
|
+
// path silently skips (#1385). It finds spec.md at any depth, covering
|
|
123
|
+
// both specs/<capability>/spec.md and the nested multi-area
|
|
124
|
+
// specs/<area>/<capability>/spec.md layout (#1182b).
|
|
125
|
+
const discoveredSpecs = await discoverSpecFiles(specsDir);
|
|
126
|
+
// A spec.md directly at the specs/ root has no capability folder, so the
|
|
127
|
+
// merge path drops it: without this error the change validates clean and
|
|
128
|
+
// archives while its requirements never reach openspec/specs/ (#1385).
|
|
129
|
+
// Only a regular file counts — a *directory* named spec.md is a capability
|
|
130
|
+
// folder like any other, and discoverSpecFiles reads it normally.
|
|
131
|
+
const rootSpecStat = await fs.stat(path.join(specsDir, 'spec.md')).catch(() => null);
|
|
132
|
+
hasRootLevelSpec = rootSpecStat?.isFile() === true;
|
|
133
|
+
if (hasRootLevelSpec) {
|
|
134
|
+
issues.push({
|
|
135
|
+
level: 'ERROR',
|
|
136
|
+
path: 'spec.md',
|
|
137
|
+
message: 'Delta spec found at specs/spec.md. Delta specs must live under a capability path (e.g. specs/<capability-path>/spec.md) — a file at the specs/ root is ignored when the change is applied or archived.',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
for (const { id: specId, specFile } of discoveredSpecs) {
|
|
141
|
+
let content;
|
|
142
|
+
try {
|
|
143
|
+
content = await fs.readFile(specFile, 'utf-8');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const plan = parseDeltaSpec(content);
|
|
149
|
+
const entryPath = FileSystemUtils.toPosixPath(path.relative(specsDir, specFile));
|
|
150
|
+
// Surface (as INFO, never a failure) the non-canonical level-3 headers
|
|
151
|
+
// the delta reader skipped while parsing ADDED/MODIFIED sections —
|
|
152
|
+
// without this note a stray divider like "### Documentation
|
|
153
|
+
// Requirements" would pass validate <change> while failing
|
|
154
|
+
// archive/validate <spec>. The list comes from the parse itself, so it
|
|
155
|
+
// reflects exactly what the reader skipped.
|
|
156
|
+
for (const stray of plan.skippedHeaders) {
|
|
157
|
+
const nameless = /^requirement:?$/i.test(stray.header);
|
|
158
|
+
issues.push({
|
|
159
|
+
level: 'INFO',
|
|
160
|
+
path: entryPath,
|
|
161
|
+
line: stray.line,
|
|
162
|
+
message: nameless
|
|
163
|
+
? `Header "### ${stray.header}" in ${stray.section} is missing a requirement name and is ignored by validation. Add a name, e.g. "### Requirement: <name>".`
|
|
164
|
+
: `Header "### ${stray.header}" in ${stray.section} is not a "### Requirement:" header and is ignored by validation. Use "### Requirement: ${stray.header}" if it should be validated as a requirement.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const sectionNames = [];
|
|
168
|
+
if (plan.sectionPresence.added)
|
|
169
|
+
sectionNames.push('## ADDED Requirements');
|
|
170
|
+
if (plan.sectionPresence.modified)
|
|
171
|
+
sectionNames.push('## MODIFIED Requirements');
|
|
172
|
+
if (plan.sectionPresence.removed)
|
|
173
|
+
sectionNames.push('## REMOVED Requirements');
|
|
174
|
+
if (plan.sectionPresence.renamed)
|
|
175
|
+
sectionNames.push('## RENAMED Requirements');
|
|
176
|
+
const hasSections = sectionNames.length > 0;
|
|
177
|
+
const hasEntries = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
|
|
178
|
+
if (!hasEntries) {
|
|
179
|
+
if (hasSections)
|
|
180
|
+
emptySectionSpecs.push({ path: entryPath, sections: sectionNames });
|
|
181
|
+
else
|
|
182
|
+
missingHeaderSpecs.push(entryPath);
|
|
183
|
+
}
|
|
184
|
+
const addedNames = new Set();
|
|
185
|
+
const modifiedNames = new Set();
|
|
186
|
+
const removedNames = new Set();
|
|
187
|
+
const renamedFrom = new Set();
|
|
188
|
+
const renamedTo = new Set();
|
|
189
|
+
// Validate ADDED
|
|
190
|
+
for (const block of plan.added) {
|
|
191
|
+
const key = normalizeRequirementName(block.name);
|
|
192
|
+
totalDeltas++;
|
|
193
|
+
if (addedNames.has(key)) {
|
|
194
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in ADDED: "${block.name}"` });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
addedNames.add(key);
|
|
198
|
+
}
|
|
199
|
+
const requirementText = this.extractRequirementText(block.raw);
|
|
200
|
+
if (!requirementText) {
|
|
201
|
+
issues.push({
|
|
202
|
+
level: 'ERROR',
|
|
203
|
+
path: entryPath,
|
|
204
|
+
message: this.containsShallOrMust(block.name)
|
|
205
|
+
? this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name)
|
|
206
|
+
: `ADDED "${block.name}" is missing requirement text`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else if (!this.containsShallOrMust(requirementText)) {
|
|
210
|
+
issues.push({
|
|
211
|
+
level: 'WARNING',
|
|
212
|
+
path: entryPath,
|
|
213
|
+
message: this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name, true),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const scenarioCount = this.countScenarios(block.raw);
|
|
217
|
+
if (scenarioCount < 1) {
|
|
218
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario` });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Validate MODIFIED
|
|
222
|
+
for (const block of plan.modified) {
|
|
223
|
+
const key = normalizeRequirementName(block.name);
|
|
224
|
+
totalDeltas++;
|
|
225
|
+
if (modifiedNames.has(key)) {
|
|
226
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in MODIFIED: "${block.name}"` });
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
modifiedNames.add(key);
|
|
230
|
+
}
|
|
231
|
+
const requirementText = this.extractRequirementText(block.raw);
|
|
232
|
+
if (!requirementText) {
|
|
233
|
+
issues.push({
|
|
234
|
+
level: 'ERROR',
|
|
235
|
+
path: entryPath,
|
|
236
|
+
message: this.containsShallOrMust(block.name)
|
|
237
|
+
? this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name)
|
|
238
|
+
: `MODIFIED "${block.name}" is missing requirement text`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
else if (!this.containsShallOrMust(requirementText)) {
|
|
242
|
+
issues.push({
|
|
243
|
+
level: 'WARNING',
|
|
244
|
+
path: entryPath,
|
|
245
|
+
message: this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name, true),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const scenarioCount = this.countScenarios(block.raw);
|
|
249
|
+
if (scenarioCount < 1) {
|
|
250
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario` });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Run archive's scenario-loss check here too, so the change fails at
|
|
254
|
+
// authoring time instead of days later at archive time (#1477).
|
|
255
|
+
if (options.mainSpecsDir && plan.modified.length > 0) {
|
|
256
|
+
const mainSpecFile = path.join(options.mainSpecsDir, ...specId.split('/'), 'spec.md');
|
|
257
|
+
FileSystemUtils.assertPathWithin(path.dirname(mainSpecFile), mainSpecFile);
|
|
258
|
+
issues.push(...(await this.findScenarioLossIssues(plan.modified, plan.renamed, mainSpecFile, entryPath, path.dirname(mainSpecFile))));
|
|
259
|
+
}
|
|
260
|
+
// Validate REMOVED (names only)
|
|
261
|
+
for (const name of plan.removed) {
|
|
262
|
+
const key = normalizeRequirementName(name);
|
|
263
|
+
totalDeltas++;
|
|
264
|
+
if (removedNames.has(key)) {
|
|
265
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in REMOVED: "${name}"` });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
removedNames.add(key);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Validate RENAMED pairs
|
|
272
|
+
for (const { from, to } of plan.renamed) {
|
|
273
|
+
const fromKey = normalizeRequirementName(from);
|
|
274
|
+
const toKey = normalizeRequirementName(to);
|
|
275
|
+
totalDeltas++;
|
|
276
|
+
if (renamedFrom.has(fromKey)) {
|
|
277
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate FROM in RENAMED: "${from}"` });
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
renamedFrom.add(fromKey);
|
|
281
|
+
}
|
|
282
|
+
if (renamedTo.has(toKey)) {
|
|
283
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate TO in RENAMED: "${to}"` });
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
renamedTo.add(toKey);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Cross-section conflicts (within the same spec file)
|
|
290
|
+
for (const n of modifiedNames) {
|
|
291
|
+
if (removedNames.has(n)) {
|
|
292
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and REMOVED: "${n}"` });
|
|
293
|
+
}
|
|
294
|
+
if (addedNames.has(n)) {
|
|
295
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and ADDED: "${n}"` });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const n of addedNames) {
|
|
299
|
+
if (removedNames.has(n)) {
|
|
300
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both ADDED and REMOVED: "${n}"` });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const { from, to } of plan.renamed) {
|
|
304
|
+
const fromKey = normalizeRequirementName(from);
|
|
305
|
+
const toKey = normalizeRequirementName(to);
|
|
306
|
+
if (modifiedNames.has(fromKey)) {
|
|
307
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED references old name from RENAMED. Use new header for "${to}"` });
|
|
308
|
+
}
|
|
309
|
+
if (addedNames.has(toKey)) {
|
|
310
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
|
|
311
|
+
}
|
|
312
|
+
// Folded comparison: a case/whitespace variant of the FROM header
|
|
313
|
+
// in REMOVED is the same contradiction, not a different name.
|
|
314
|
+
const removedFoldMatch = [...removedNames].find((r) => foldRequirementName(r) === foldRequirementName(fromKey));
|
|
315
|
+
if (removedFoldMatch !== undefined) {
|
|
316
|
+
issues.push({
|
|
317
|
+
level: 'ERROR',
|
|
318
|
+
path: entryPath,
|
|
319
|
+
message: `Requirement present in both RENAMED and REMOVED: "${from}"` +
|
|
320
|
+
(removedFoldMatch === fromKey ? '' : ` (REMOVED spells it "${removedFoldMatch}")`),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Reuse archive's merge builder to report conflicts with the main specs.
|
|
326
|
+
// Keep structural errors and scenario loss in their existing diagnostics.
|
|
327
|
+
if (options.mainSpecsDir) {
|
|
328
|
+
issues.push(...(await this.findArchiveBlockers(changeDir, options.mainSpecsDir, [
|
|
329
|
+
...issues.filter((issue) => issue.level === 'ERROR').map((issue) => issue.path),
|
|
330
|
+
// Collected in the loop above but not turned into issues until
|
|
331
|
+
// after this try block, so they are invisible to the filter. A
|
|
332
|
+
// delta with no parsed sections has nothing for the merge to
|
|
333
|
+
// apply, which it reports as a failure of its own - on top of the
|
|
334
|
+
// error that actually names the mistake.
|
|
335
|
+
...missingHeaderSpecs,
|
|
336
|
+
...emptySectionSpecs.map((spec) => spec.path),
|
|
337
|
+
])));
|
|
338
|
+
issues.push(...(await this.findUnresolvedHeaderIssues(changeDir, options.mainSpecsDir)));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
// A missing specs dir (or a stray `specs` file) means no deltas;
|
|
343
|
+
// anything else (EACCES, EIO) must stay loud — discoverSpecFiles
|
|
344
|
+
// documents that silently dropping an unreadable capability recreates
|
|
345
|
+
// the data-loss class it prevents, and archive lets the same error
|
|
346
|
+
// propagate.
|
|
347
|
+
const code = error?.code;
|
|
348
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const { path: specPath, sections } of emptySectionSpecs) {
|
|
353
|
+
issues.push({
|
|
354
|
+
level: 'ERROR',
|
|
355
|
+
path: specPath,
|
|
356
|
+
message: `Delta sections ${this.formatSectionList(sections)} were found, but no requirement entries parsed. Ensure each section includes at least one "### Requirement:" block (REMOVED may use bullet list syntax).`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
for (const path of missingHeaderSpecs) {
|
|
360
|
+
issues.push({
|
|
361
|
+
level: 'ERROR',
|
|
362
|
+
path,
|
|
363
|
+
message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
// The root-level error already names the file and the fix; adding "No
|
|
367
|
+
// deltas found" on top would contradict it, since the deltas are sitting in
|
|
368
|
+
// the file just reported. (skip_specs marker handling is intentionally not
|
|
369
|
+
// vendored in this slice — see the file header.)
|
|
370
|
+
if (totalDeltas === 0 && !hasRootLevelSpec) {
|
|
371
|
+
issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
|
|
372
|
+
}
|
|
373
|
+
if (options.projectRoot) {
|
|
374
|
+
issues.push(...await this.collectTaskNumberingIssues(changeDir));
|
|
375
|
+
}
|
|
376
|
+
return this.createReport(issues);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Task-numbering check, reconstructed minimally for this slice. Upstream
|
|
380
|
+
* resolves the schema's tracked task files (artifact graph) and gates the
|
|
381
|
+
* check to the built-in spec-driven schema; here we read the change's
|
|
382
|
+
* top-level tasks.md — the spec-driven schema's `apply.tracks` target — and
|
|
383
|
+
* run the identical `findTaskNumberingIssues` analysis.
|
|
384
|
+
*/
|
|
385
|
+
async collectTaskNumberingIssues(changeDir) {
|
|
386
|
+
const taskFile = path.join(changeDir, 'tasks.md');
|
|
387
|
+
let content;
|
|
388
|
+
try {
|
|
389
|
+
content = await fs.readFile(taskFile, 'utf-8');
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
return findTaskNumberingIssues([{ path: 'tasks.md', content }]).map((issue) => ({
|
|
395
|
+
level: 'WARNING',
|
|
396
|
+
path: issue.path,
|
|
397
|
+
line: issue.line,
|
|
398
|
+
message: issue.message,
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Report MODIFIED requirements whose block omits a scenario the main spec
|
|
403
|
+
* still carries. Uses the same comparison archive applies, so validate can
|
|
404
|
+
* only report what archive would refuse.
|
|
405
|
+
*
|
|
406
|
+
* Silent when the main spec or the requirement header is absent: applying a
|
|
407
|
+
* MODIFIED against a base that is not there yet is a different failure (a
|
|
408
|
+
* sister change still in flight is the legitimate case), and archive is the
|
|
409
|
+
* gate for it. A spec that exists but cannot be read is not absent, though —
|
|
410
|
+
* archive aborts on it, so reporting it beats calling the change valid.
|
|
411
|
+
*/
|
|
412
|
+
async findScenarioLossIssues(modified, renamed, mainSpecFile, entryPath, mainSpecRoot) {
|
|
413
|
+
let mainContent;
|
|
414
|
+
FileSystemUtils.assertPathWithin(mainSpecRoot, mainSpecFile);
|
|
415
|
+
try {
|
|
416
|
+
mainContent = await fs.readFile(mainSpecFile, 'utf-8');
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
const code = error?.code;
|
|
420
|
+
// Reported only for the codes that mean the file itself is unusable, and
|
|
421
|
+
// will be just as unusable when archive reads it. Everything else -
|
|
422
|
+
// ENOENT/ENOTDIR ("no main spec"), and transient resource errors like
|
|
423
|
+
// EMFILE that say nothing about the file - stays silent rather than
|
|
424
|
+
// failing a change that is fine. `validate --all` reads six changes at
|
|
425
|
+
// once, so a resource error must never become a verdict.
|
|
426
|
+
const UNUSABLE = new Set(['EACCES', 'EPERM', 'EISDIR', 'ELOOP', 'ENAMETOOLONG']);
|
|
427
|
+
if (!code || !UNUSABLE.has(code))
|
|
428
|
+
return [];
|
|
429
|
+
return [
|
|
430
|
+
{
|
|
431
|
+
level: 'ERROR',
|
|
432
|
+
path: entryPath,
|
|
433
|
+
message: `Could not read ${FileSystemUtils.toPosixPath(mainSpecFile)} to check the MODIFIED requirements against it ` +
|
|
434
|
+
`(${code}). Archive reads the same file, so fix the file before archiving.`,
|
|
435
|
+
},
|
|
436
|
+
];
|
|
437
|
+
}
|
|
438
|
+
const currentBlocks = new Map();
|
|
439
|
+
for (const block of extractRequirementsSection(mainContent).bodyBlocks) {
|
|
440
|
+
currentBlocks.set(normalizeRequirementName(block.name), block);
|
|
441
|
+
}
|
|
442
|
+
// Archive applies RENAMED before MODIFIED, so a MODIFIED naming the new
|
|
443
|
+
// header is compared against the renamed block's scenarios. Fall back to
|
|
444
|
+
// the old header, or a rename-plus-modify pair would skip the check.
|
|
445
|
+
const renamedFrom = new Map(renamed.map(({ from, to }) => [normalizeRequirementName(to), normalizeRequirementName(from)]));
|
|
446
|
+
// Walked, not looked up once: renames chain (A→B then B→C leaves C holding
|
|
447
|
+
// A's block), and the visited set stops a cycle from looping forever. Every
|
|
448
|
+
// name in a rename cycle is also a rename FROM, so the skip above already
|
|
449
|
+
// keeps the walk out of one; the guard stays because the cost of being
|
|
450
|
+
// wrong about that is a hung CLI, not a wrong message.
|
|
451
|
+
const currentBlockFor = (name) => {
|
|
452
|
+
const visited = new Set();
|
|
453
|
+
let key = name;
|
|
454
|
+
while (key !== undefined && !visited.has(key)) {
|
|
455
|
+
const block = currentBlocks.get(key);
|
|
456
|
+
if (block)
|
|
457
|
+
return block;
|
|
458
|
+
visited.add(key);
|
|
459
|
+
key = renamedFrom.get(key);
|
|
460
|
+
}
|
|
461
|
+
return undefined;
|
|
462
|
+
};
|
|
463
|
+
// A MODIFIED naming a header the same delta renames away is already
|
|
464
|
+
// reported ("MODIFIED references old name from RENAMED"), and the block it
|
|
465
|
+
// would land on is not the one it names — so any scenario named here would
|
|
466
|
+
// send the author after the wrong requirement.
|
|
467
|
+
const renamedAway = new Set(renamed.map(({ from }) => normalizeRequirementName(from)));
|
|
468
|
+
const issues = [];
|
|
469
|
+
for (const block of modified) {
|
|
470
|
+
const key = normalizeRequirementName(block.name);
|
|
471
|
+
if (renamedAway.has(key))
|
|
472
|
+
continue;
|
|
473
|
+
const current = currentBlockFor(key);
|
|
474
|
+
if (!current)
|
|
475
|
+
continue;
|
|
476
|
+
const missing = findMissingCurrentScenarios(current, block);
|
|
477
|
+
if (missing.length === 0)
|
|
478
|
+
continue;
|
|
479
|
+
issues.push({
|
|
480
|
+
level: 'ERROR',
|
|
481
|
+
path: entryPath,
|
|
482
|
+
message: `MODIFIED "${block.name}" omits scenario(s) the current spec still has: ` +
|
|
483
|
+
`${missing.map(name => `"${name}"`).join(', ')}. ` +
|
|
484
|
+
'Copy them into the MODIFIED block (a MODIFIED requirement replaces the whole block, so archive refuses to drop them).',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return issues;
|
|
488
|
+
}
|
|
489
|
+
convertZodErrors(error) {
|
|
490
|
+
return error.issues.map(err => {
|
|
491
|
+
let message = err.message;
|
|
492
|
+
if (message === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
|
|
493
|
+
message = `${message}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
level: 'ERROR',
|
|
497
|
+
path: err.path.join('.'),
|
|
498
|
+
message,
|
|
499
|
+
};
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
applySpecRules(spec, content) {
|
|
503
|
+
const issues = [];
|
|
504
|
+
for (const structuralIssue of findMainSpecStructureIssues(content)) {
|
|
505
|
+
issues.push({
|
|
506
|
+
level: 'ERROR',
|
|
507
|
+
path: 'file',
|
|
508
|
+
line: structuralIssue.line,
|
|
509
|
+
message: structuralIssue.message,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
// The placeholder is longer than MIN_PURPOSE_LENGTH, so the brevity check
|
|
513
|
+
// below cannot reach it; it is reported on its own terms instead. Checked
|
|
514
|
+
// first because a hand-written "TBD" is both a placeholder and too brief,
|
|
515
|
+
// and only one of those two tells the author what to do. (A "TODO" opening
|
|
516
|
+
// the Purpose reads the same way, so it is the same finding.)
|
|
517
|
+
const placeholder = findPurposePlaceholderIssue(spec.overview, content);
|
|
518
|
+
if (placeholder) {
|
|
519
|
+
issues.push({
|
|
520
|
+
level: 'WARNING',
|
|
521
|
+
path: 'overview',
|
|
522
|
+
line: placeholder.line,
|
|
523
|
+
message: VALIDATION_MESSAGES.PURPOSE_IS_PLACEHOLDER,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
else if (spec.overview.length < MIN_PURPOSE_LENGTH) {
|
|
527
|
+
issues.push({
|
|
528
|
+
level: 'WARNING',
|
|
529
|
+
path: 'overview',
|
|
530
|
+
message: VALIDATION_MESSAGES.PURPOSE_TOO_BRIEF,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
spec.requirements.forEach((req, index) => {
|
|
534
|
+
if (req.text.length > MAX_REQUIREMENT_TEXT_LENGTH) {
|
|
535
|
+
issues.push({
|
|
536
|
+
level: 'INFO',
|
|
537
|
+
path: `requirements[${index}]`,
|
|
538
|
+
message: VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (req.scenarios.length === 0) {
|
|
542
|
+
issues.push({
|
|
543
|
+
level: 'WARNING',
|
|
544
|
+
path: `requirements[${index}].scenarios`,
|
|
545
|
+
message: `${VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS}. ${VALIDATION_MESSAGES.GUIDE_SCENARIO_FORMAT}`,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
// SHALL/MUST body-keyword guidance for main specs (#1156, #243). The main-spec
|
|
550
|
+
// parser collapses the requirement header into `text`, so we recover the
|
|
551
|
+
// header+body pairs here (the same source the delta path trusts) and reuse
|
|
552
|
+
// the delta detection. A non-empty body that omits the English keyword gets
|
|
553
|
+
// guidance, while a missing body remains an error. Emitted exactly once per
|
|
554
|
+
// requirement (the Zod refine that used to emit a generic error is removed).
|
|
555
|
+
extractRequirementsSection(content).bodyBlocks.forEach((block, index) => {
|
|
556
|
+
const requirementText = this.extractRequirementText(block.raw);
|
|
557
|
+
if (!requirementText) {
|
|
558
|
+
issues.push({
|
|
559
|
+
level: 'ERROR',
|
|
560
|
+
path: `requirements[${index}]`,
|
|
561
|
+
message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
else if (!this.containsShallOrMust(requirementText)) {
|
|
565
|
+
issues.push({
|
|
566
|
+
level: 'WARNING',
|
|
567
|
+
path: `requirements[${index}]`,
|
|
568
|
+
message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name, true),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
return issues;
|
|
573
|
+
}
|
|
574
|
+
applyChangeRules(change, content) {
|
|
575
|
+
const issues = [];
|
|
576
|
+
const MIN_DELTA_DESCRIPTION_LENGTH = 10;
|
|
577
|
+
change.deltas.forEach((delta, index) => {
|
|
578
|
+
if (!delta.description || delta.description.length < MIN_DELTA_DESCRIPTION_LENGTH) {
|
|
579
|
+
issues.push({
|
|
580
|
+
level: 'WARNING',
|
|
581
|
+
path: `deltas[${index}].description`,
|
|
582
|
+
message: VALIDATION_MESSAGES.DELTA_DESCRIPTION_TOO_BRIEF,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if ((delta.operation === 'ADDED' || delta.operation === 'MODIFIED') &&
|
|
586
|
+
(!delta.requirements || delta.requirements.length === 0)) {
|
|
587
|
+
issues.push({
|
|
588
|
+
level: 'WARNING',
|
|
589
|
+
path: `deltas[${index}].requirements`,
|
|
590
|
+
message: `${delta.operation} ${VALIDATION_MESSAGES.DELTA_MISSING_REQUIREMENTS}`,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
return issues;
|
|
595
|
+
}
|
|
596
|
+
enrichTopLevelError(itemId, baseMessage) {
|
|
597
|
+
const msg = baseMessage.trim();
|
|
598
|
+
if (msg === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
|
|
599
|
+
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
|
|
600
|
+
}
|
|
601
|
+
if (msg.includes('Spec must have a Purpose section') || msg.includes('Spec must have a Requirements section')) {
|
|
602
|
+
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_SPEC_SECTIONS}`;
|
|
603
|
+
}
|
|
604
|
+
if (msg.includes('Change must have a Why section') || msg.includes('Change must have a What Changes section')) {
|
|
605
|
+
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_CHANGE_SECTIONS}`;
|
|
606
|
+
}
|
|
607
|
+
return msg;
|
|
608
|
+
}
|
|
609
|
+
extractNameFromPath(filePath) {
|
|
610
|
+
const normalizedPath = FileSystemUtils.toPosixPath(filePath);
|
|
611
|
+
const parts = normalizedPath.split('/');
|
|
612
|
+
// Look for the directory name after 'specs' or 'changes'
|
|
613
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
614
|
+
if (parts[i] === 'specs' || parts[i] === 'changes') {
|
|
615
|
+
if (i < parts.length - 1) {
|
|
616
|
+
return parts[i + 1];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Fallback to filename without extension if not in expected structure
|
|
621
|
+
const fileName = parts[parts.length - 1] ?? '';
|
|
622
|
+
const dotIndex = fileName.lastIndexOf('.');
|
|
623
|
+
return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Dry-run archive's merge builder without writing its result. Reusing the
|
|
627
|
+
* builder preserves its already-synced delta rules instead of duplicating them.
|
|
628
|
+
* INFO leaves the verdict unchanged: a missing target can be a typo or a
|
|
629
|
+
* requirement introduced by a sibling change that has not archived yet.
|
|
630
|
+
* This does not run archive's later merged-spec validation or retirement checks.
|
|
631
|
+
*/
|
|
632
|
+
async findArchiveBlockers(changeDir, mainSpecsDir, alreadyReportedPaths) {
|
|
633
|
+
const alreadyReported = new Set(alreadyReportedPaths);
|
|
634
|
+
// Only ever reaches a generated skeleton's placeholder Purpose, which this
|
|
635
|
+
// dry run discards.
|
|
636
|
+
const changeName = path.basename(changeDir);
|
|
637
|
+
const issues = [];
|
|
638
|
+
let updates;
|
|
639
|
+
try {
|
|
640
|
+
updates = await findSpecUpdates(changeDir, mainSpecsDir);
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
// An incomplete advisory check must not discard the validation report.
|
|
644
|
+
// Source discovery already ran above; archive retains its own path guards.
|
|
645
|
+
return [{
|
|
646
|
+
level: 'INFO',
|
|
647
|
+
path: 'specs',
|
|
648
|
+
message: `Could not check archive merge conflicts: ${error instanceof Error ? error.message : String(error)}`,
|
|
649
|
+
}];
|
|
650
|
+
}
|
|
651
|
+
for (const update of updates) {
|
|
652
|
+
// discoverSpecFiles builds both this id and the entryPath the checks
|
|
653
|
+
// above report under, from the same walk.
|
|
654
|
+
const entryPath = FileSystemUtils.toPosixPath(`${update.id}/spec.md`);
|
|
655
|
+
// A delta those checks already rejected would be reported twice, the
|
|
656
|
+
// second time in archive's wording rather than the wording that names
|
|
657
|
+
// the actual mistake.
|
|
658
|
+
if (alreadyReported.has(entryPath))
|
|
659
|
+
continue;
|
|
660
|
+
try {
|
|
661
|
+
await buildUpdatedSpec(update, changeName, { silent: true });
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
// Only the thrown preconditions, which carry no errno. A filesystem
|
|
665
|
+
// error says nothing about whether the delta applies, and `validate
|
|
666
|
+
// --all` reads six changes at once, so a transient EMFILE would report
|
|
667
|
+
// a collision that is not there - the same reason the scenario-loss
|
|
668
|
+
// check above reads only the codes that mean the file is unusable.
|
|
669
|
+
if (error?.code !== undefined)
|
|
670
|
+
continue;
|
|
671
|
+
issues.push({
|
|
672
|
+
level: 'INFO',
|
|
673
|
+
path: entryPath,
|
|
674
|
+
message: `Archive would refuse this delta: ${error instanceof Error ? error.message : String(error)}`,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return issues;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* FreeHarness addition: MODIFIED / REMOVED / RENAMED headers absent from the
|
|
682
|
+
* current main spec (delta-resolution.ts). ERROR in strict mode, WARNING
|
|
683
|
+
* otherwise: the default verdict tolerates a sister change still in flight,
|
|
684
|
+
* strict does not. The change's parent directory is the changes root, which
|
|
685
|
+
* is where the ADDED-elsewhere attribution looks.
|
|
686
|
+
*/
|
|
687
|
+
async findUnresolvedHeaderIssues(changeDir, mainSpecsDir) {
|
|
688
|
+
const level = this.strictMode ? 'ERROR' : 'WARNING';
|
|
689
|
+
const unresolved = await findUnresolvedDeltaHeaders(changeDir, mainSpecsDir, path.dirname(changeDir));
|
|
690
|
+
return unresolved.map((u) => ({ level, path: u.entryPath, message: u.message }));
|
|
691
|
+
}
|
|
692
|
+
createReport(issues) {
|
|
693
|
+
const errors = issues.filter(i => i.level === 'ERROR').length;
|
|
694
|
+
const warnings = issues.filter(i => i.level === 'WARNING').length;
|
|
695
|
+
const info = issues.filter(i => i.level === 'INFO').length;
|
|
696
|
+
const valid = this.strictMode
|
|
697
|
+
? errors === 0 && warnings === 0
|
|
698
|
+
: errors === 0;
|
|
699
|
+
return {
|
|
700
|
+
valid,
|
|
701
|
+
issues,
|
|
702
|
+
summary: {
|
|
703
|
+
errors,
|
|
704
|
+
warnings,
|
|
705
|
+
info,
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
isValid(report) {
|
|
710
|
+
return report.valid;
|
|
711
|
+
}
|
|
712
|
+
extractRequirementText(blockRaw) {
|
|
713
|
+
// Delegate to the shared, fence-/metadata-/multi-line-aware body reader.
|
|
714
|
+
// Validation intentionally does not use the parser/display header-title
|
|
715
|
+
// fallback for canonical `### Requirement:` blocks: #1280 requires a
|
|
716
|
+
// SHALL/MUST that appears only in the header to receive the body-keyword
|
|
717
|
+
// hint. Line 0 is the `### Requirement: ...` header.
|
|
718
|
+
const [, ...bodyLines] = blockRaw.split('\n');
|
|
719
|
+
return extractRequirementBodyShared(bodyLines) || undefined;
|
|
720
|
+
}
|
|
721
|
+
containsShallOrMust(text) {
|
|
722
|
+
return containsShallOrMustShared(text);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Build a message for a requirement block whose body lacks SHALL/MUST.
|
|
726
|
+
*
|
|
727
|
+
* When the SHALL/MUST keyword already appears in the requirement header (e.g.
|
|
728
|
+
* `### Requirement: The system SHALL ...`) the original generic error
|
|
729
|
+
* ("must contain SHALL or MUST") is confusing because the keyword is visibly
|
|
730
|
+
* present in the spec. Per the OpenSpec conventions the keyword has to live
|
|
731
|
+
* on the requirement body line (the line right after the header), so we point
|
|
732
|
+
* the author at that exact fix when the keyword is found in the header only.
|
|
733
|
+
*/
|
|
734
|
+
buildMissingShallOrMustMessage(prefix, blockName, guidanceOnly = false) {
|
|
735
|
+
const base = `${prefix} ${guidanceOnly ? 'should' : 'must'} contain SHALL or MUST`;
|
|
736
|
+
const suffix = guidanceOnly ? ' (RFC 2119 best practice for English specs)' : '';
|
|
737
|
+
if (this.containsShallOrMust(blockName)) {
|
|
738
|
+
return `${base} in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.${suffix}`;
|
|
739
|
+
}
|
|
740
|
+
return `${base}${suffix}`;
|
|
741
|
+
}
|
|
742
|
+
countScenarios(blockRaw) {
|
|
743
|
+
// Fence-aware count via the shared reader: a `#### Scenario:` inside a fenced
|
|
744
|
+
// example is not a real scenario. Drop the header line (index 0).
|
|
745
|
+
return countScenariosShared(blockRaw.split('\n').slice(1));
|
|
746
|
+
}
|
|
747
|
+
formatSectionList(sections) {
|
|
748
|
+
if (sections.length === 0)
|
|
749
|
+
return '';
|
|
750
|
+
if (sections.length === 1)
|
|
751
|
+
return sections[0];
|
|
752
|
+
const head = sections.slice(0, -1);
|
|
753
|
+
const last = sections[sections.length - 1];
|
|
754
|
+
return `${head.join(', ')} and ${last}`;
|
|
755
|
+
}
|
|
756
|
+
}
|