@kbdavis07/openspec 1.5.0-nov.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 +22 -0
- package/README.md +239 -0
- package/bin/openspec.js +5 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +544 -0
- package/dist/commands/change.d.ts +38 -0
- package/dist/commands/change.js +245 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +264 -0
- package/dist/commands/config.d.ts +36 -0
- package/dist/commands/config.js +552 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +155 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/feedback.d.ts +9 -0
- package/dist/commands/feedback.js +183 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.js +869 -0
- package/dist/commands/shared-gather.d.ts +14 -0
- package/dist/commands/shared-gather.js +31 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/show.d.ts +19 -0
- package/dist/commands/show.js +177 -0
- package/dist/commands/spec.d.ts +19 -0
- package/dist/commands/spec.js +236 -0
- package/dist/commands/store.d.ts +3 -0
- package/dist/commands/store.js +547 -0
- package/dist/commands/validate.d.ts +34 -0
- package/dist/commands/validate.js +342 -0
- package/dist/commands/workflow/index.d.ts +17 -0
- package/dist/commands/workflow/index.js +12 -0
- package/dist/commands/workflow/instructions.d.ts +39 -0
- package/dist/commands/workflow/instructions.js +393 -0
- package/dist/commands/workflow/new-change.d.ts +20 -0
- package/dist/commands/workflow/new-change.js +109 -0
- package/dist/commands/workflow/schemas.d.ts +10 -0
- package/dist/commands/workflow/schemas.js +34 -0
- package/dist/commands/workflow/shared.d.ts +74 -0
- package/dist/commands/workflow/shared.js +170 -0
- package/dist/commands/workflow/status.d.ts +16 -0
- package/dist/commands/workflow/status.js +92 -0
- package/dist/commands/workflow/templates.d.ts +16 -0
- package/dist/commands/workflow/templates.js +69 -0
- package/dist/commands/workset-input.d.ts +19 -0
- package/dist/commands/workset-input.js +112 -0
- package/dist/commands/workset-prompts.d.ts +12 -0
- package/dist/commands/workset-prompts.js +143 -0
- package/dist/commands/workset.d.ts +25 -0
- package/dist/commands/workset.js +446 -0
- package/dist/core/archive.d.ts +22 -0
- package/dist/core/archive.js +496 -0
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +9 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +188 -0
- package/dist/core/artifact-graph/instruction-loader.js +233 -0
- package/dist/core/artifact-graph/outputs.d.ts +14 -0
- package/dist/core/artifact-graph/outputs.js +39 -0
- package/dist/core/artifact-graph/resolver.d.ts +81 -0
- package/dist/core/artifact-graph/resolver.js +257 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +31 -0
- package/dist/core/artifact-graph/types.d.ts +40 -0
- package/dist/core/artifact-graph/types.js +29 -0
- package/dist/core/available-tools.d.ts +17 -0
- package/dist/core/available-tools.js +43 -0
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +19 -0
- package/dist/core/change-metadata/schema.js +30 -0
- package/dist/core/change-status-policy.d.ts +37 -0
- package/dist/core/change-status-policy.js +35 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
- package/dist/core/command-generation/adapters/amazon-q.js +26 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +26 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +27 -0
- package/dist/core/command-generation/adapters/bob.d.ts +14 -0
- package/dist/core/command-generation/adapters/bob.js +32 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +39 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +28 -0
- package/dist/core/command-generation/adapters/codex.d.ts +16 -0
- package/dist/core/command-generation/adapters/codex.js +39 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +28 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +27 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +31 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +27 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +26 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +26 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +29 -0
- package/dist/core/command-generation/adapters/index.d.ts +34 -0
- package/dist/core/command-generation/adapters/index.js +34 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +26 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +26 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +44 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
- package/dist/core/command-generation/adapters/opencode.js +29 -0
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +42 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
- package/dist/core/command-generation/adapters/qwen.js +26 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +48 -0
- package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
- package/dist/core/command-generation/adapters/windsurf.js +38 -0
- package/dist/core/command-generation/generator.d.ts +21 -0
- package/dist/core/command-generation/generator.js +27 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- package/dist/core/command-generation/registry.d.ts +36 -0
- package/dist/core/command-generation/registry.js +102 -0
- package/dist/core/command-generation/types.d.ts +56 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/command-generation/yaml.d.ts +22 -0
- package/dist/core/command-generation/yaml.js +38 -0
- package/dist/core/completions/command-registry.d.ts +3 -0
- package/dist/core/completions/command-registry.js +759 -0
- package/dist/core/completions/completion-provider.d.ts +71 -0
- package/dist/core/completions/completion-provider.js +129 -0
- package/dist/core/completions/factory.d.ts +64 -0
- package/dist/core/completions/factory.js +75 -0
- package/dist/core/completions/generators/bash-generator.d.ts +35 -0
- package/dist/core/completions/generators/bash-generator.js +230 -0
- package/dist/core/completions/generators/fish-generator.d.ts +32 -0
- package/dist/core/completions/generators/fish-generator.js +160 -0
- package/dist/core/completions/generators/powershell-generator.d.ts +36 -0
- package/dist/core/completions/generators/powershell-generator.js +266 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +47 -0
- package/dist/core/completions/generators/zsh-generator.js +276 -0
- package/dist/core/completions/installers/bash-installer.d.ts +87 -0
- package/dist/core/completions/installers/bash-installer.js +321 -0
- package/dist/core/completions/installers/fish-installer.d.ts +43 -0
- package/dist/core/completions/installers/fish-installer.js +151 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +102 -0
- package/dist/core/completions/installers/powershell-installer.js +415 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +117 -0
- package/dist/core/completions/installers/zsh-installer.js +424 -0
- package/dist/core/completions/shared-flags.d.ts +13 -0
- package/dist/core/completions/shared-flags.js +33 -0
- package/dist/core/completions/templates/bash-templates.d.ts +6 -0
- package/dist/core/completions/templates/bash-templates.js +30 -0
- package/dist/core/completions/templates/fish-templates.d.ts +7 -0
- package/dist/core/completions/templates/fish-templates.js +45 -0
- package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
- package/dist/core/completions/templates/powershell-templates.js +34 -0
- package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
- package/dist/core/completions/templates/zsh-templates.js +45 -0
- package/dist/core/completions/types.d.ts +101 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +34 -0
- package/dist/core/config-schema.d.ts +87 -0
- package/dist/core/config-schema.js +239 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +40 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +112 -0
- package/dist/core/global-config.d.ts +51 -0
- package/dist/core/global-config.js +124 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.d.ts +37 -0
- package/dist/core/init.js +614 -0
- package/dist/core/legacy-cleanup.d.ts +162 -0
- package/dist/core/legacy-cleanup.js +514 -0
- package/dist/core/list.d.ts +11 -0
- package/dist/core/list.js +214 -0
- package/dist/core/migration.d.ts +23 -0
- package/dist/core/migration.js +108 -0
- package/dist/core/openers.d.ts +77 -0
- package/dist/core/openers.js +251 -0
- package/dist/core/openspec-root.d.ts +45 -0
- package/dist/core/openspec-root.js +195 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +198 -0
- package/dist/core/parsers/markdown-parser.d.ts +23 -0
- package/dist/core/parsers/markdown-parser.js +173 -0
- package/dist/core/parsers/requirement-blocks.d.ts +48 -0
- package/dist/core/parsers/requirement-blocks.js +235 -0
- package/dist/core/parsers/requirement-text.d.ts +55 -0
- package/dist/core/parsers/requirement-text.js +136 -0
- package/dist/core/parsers/spec-structure.d.ts +9 -0
- package/dist/core/parsers/spec-structure.js +88 -0
- package/dist/core/planning-home.d.ts +16 -0
- package/dist/core/planning-home.js +67 -0
- package/dist/core/profile-sync-drift.d.ts +38 -0
- package/dist/core/profile-sync-drift.js +201 -0
- package/dist/core/profiles.d.ts +26 -0
- package/dist/core/profiles.js +41 -0
- package/dist/core/project-config.d.ts +107 -0
- package/dist/core/project-config.js +373 -0
- package/dist/core/references.d.ts +63 -0
- package/dist/core/references.js +310 -0
- package/dist/core/relationship-health.d.ts +65 -0
- package/dist/core/relationship-health.js +64 -0
- package/dist/core/root-selection.d.ts +122 -0
- package/dist/core/root-selection.js +337 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +18 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/index.d.ts +8 -0
- package/dist/core/shared/index.js +8 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +100 -0
- package/dist/core/shared/tool-detection.d.ts +71 -0
- package/dist/core/shared/tool-detection.js +160 -0
- package/dist/core/specs-apply.d.ts +78 -0
- package/dist/core/specs-apply.js +428 -0
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/store/errors.js +22 -0
- package/dist/core/store/foundation.d.ts +56 -0
- package/dist/core/store/foundation.js +251 -0
- package/dist/core/store/git.d.ts +23 -0
- package/dist/core/store/git.js +137 -0
- package/dist/core/store/index.d.ts +5 -0
- package/dist/core/store/index.js +5 -0
- package/dist/core/store/operations.d.ts +114 -0
- package/dist/core/store/operations.js +803 -0
- package/dist/core/store/registry.d.ts +58 -0
- package/dist/core/store/registry.js +275 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/index.d.ts +8 -0
- package/dist/core/templates/index.js +9 -0
- package/dist/core/templates/skill-templates.d.ts +20 -0
- package/dist/core/templates/skill-templates.js +19 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +315 -0
- package/dist/core/templates/workflows/archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/archive-change.js +278 -0
- package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/bulk-archive-change.js +493 -0
- package/dist/core/templates/workflows/continue-change.d.ts +10 -0
- package/dist/core/templates/workflows/continue-change.js +239 -0
- package/dist/core/templates/workflows/explore.d.ts +10 -0
- package/dist/core/templates/workflows/explore.js +464 -0
- package/dist/core/templates/workflows/feedback.d.ts +9 -0
- package/dist/core/templates/workflows/feedback.js +108 -0
- package/dist/core/templates/workflows/ff-change.d.ts +10 -0
- package/dist/core/templates/workflows/ff-change.js +205 -0
- package/dist/core/templates/workflows/new-change.d.ts +10 -0
- package/dist/core/templates/workflows/new-change.js +148 -0
- package/dist/core/templates/workflows/onboard.d.ts +10 -0
- package/dist/core/templates/workflows/onboard.js +566 -0
- package/dist/core/templates/workflows/propose.d.ts +10 -0
- package/dist/core/templates/workflows/propose.js +223 -0
- package/dist/core/templates/workflows/store-selection.d.ts +8 -0
- package/dist/core/templates/workflows/store-selection.js +8 -0
- package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
- package/dist/core/templates/workflows/sync-specs.js +291 -0
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +167 -0
- package/dist/core/templates/workflows/verify-change.d.ts +10 -0
- package/dist/core/templates/workflows/verify-change.js +339 -0
- package/dist/core/update.d.ts +82 -0
- package/dist/core/update.js +557 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +51 -0
- package/dist/core/validation/validator.js +498 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/core/working-set.d.ts +47 -0
- package/dist/core/working-set.js +43 -0
- package/dist/core/worksets.d.ts +75 -0
- package/dist/core/worksets.js +245 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +28 -0
- package/dist/prompts/searchable-multi-select.js +159 -0
- package/dist/telemetry/config.d.ts +38 -0
- package/dist/telemetry/config.js +136 -0
- package/dist/telemetry/index.d.ts +31 -0
- package/dist/telemetry/index.js +164 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +10 -0
- package/dist/ui/welcome-screen.js +146 -0
- package/dist/utils/change-metadata.d.ts +55 -0
- package/dist/utils/change-metadata.js +141 -0
- package/dist/utils/change-utils.d.ts +85 -0
- package/dist/utils/change-utils.js +173 -0
- package/dist/utils/command-references.d.ts +18 -0
- package/dist/utils/command-references.js +20 -0
- package/dist/utils/file-system.d.ts +41 -0
- package/dist/utils/file-system.js +320 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +72 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +100 -0
- package/package.json +85 -0
- package/schemas/spec-driven/schema.yaml +153 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +23 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +9 -0
- package/scripts/postinstall.js +83 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ValidationReport } from './types.js';
|
|
2
|
+
export declare class Validator {
|
|
3
|
+
private strictMode;
|
|
4
|
+
constructor(strictMode?: boolean);
|
|
5
|
+
validateSpec(filePath: string): Promise<ValidationReport>;
|
|
6
|
+
/**
|
|
7
|
+
* Validate spec content from a string (used for pre-write validation of rebuilt specs)
|
|
8
|
+
*/
|
|
9
|
+
validateSpecContent(specName: string, content: string): Promise<ValidationReport>;
|
|
10
|
+
validateChange(filePath: string): Promise<ValidationReport>;
|
|
11
|
+
/**
|
|
12
|
+
* Validate delta-formatted spec files under a change directory.
|
|
13
|
+
* Enforces:
|
|
14
|
+
* - At least one delta across all files
|
|
15
|
+
* - ADDED/MODIFIED: each requirement has SHALL/MUST and at least one scenario
|
|
16
|
+
* - REMOVED: names only; no scenario/description required
|
|
17
|
+
* - RENAMED: pairs well-formed
|
|
18
|
+
* - No duplicates within sections; no cross-section conflicts per spec
|
|
19
|
+
*/
|
|
20
|
+
validateChangeDeltaSpecs(changeDir: string): Promise<ValidationReport>;
|
|
21
|
+
/**
|
|
22
|
+
* Recursively collect every delta `spec.md` under a change's specs directory,
|
|
23
|
+
* so both the one-level (specs/<capability>/spec.md) and nested multi-area
|
|
24
|
+
* (specs/<area>/<capability>/spec.md) layouts are discovered (#1182b).
|
|
25
|
+
* Returns absolute paths, sorted for deterministic issue ordering.
|
|
26
|
+
*/
|
|
27
|
+
private findDeltaSpecFiles;
|
|
28
|
+
private convertZodErrors;
|
|
29
|
+
private applySpecRules;
|
|
30
|
+
private applyChangeRules;
|
|
31
|
+
private enrichTopLevelError;
|
|
32
|
+
private extractNameFromPath;
|
|
33
|
+
private createReport;
|
|
34
|
+
isValid(report: ValidationReport): boolean;
|
|
35
|
+
private extractRequirementText;
|
|
36
|
+
private containsShallOrMust;
|
|
37
|
+
/**
|
|
38
|
+
* Build an error message for a requirement block whose body lacks SHALL/MUST.
|
|
39
|
+
*
|
|
40
|
+
* When the SHALL/MUST keyword already appears in the requirement header (e.g.
|
|
41
|
+
* `### Requirement: The system SHALL ...`) the original generic error
|
|
42
|
+
* ("must contain SHALL or MUST") is confusing because the keyword is visibly
|
|
43
|
+
* present in the spec. Per the OpenSpec conventions the keyword has to live
|
|
44
|
+
* on the requirement body line (the line right after the header), so we point
|
|
45
|
+
* the author at that exact fix when the keyword is found in the header only.
|
|
46
|
+
*/
|
|
47
|
+
private buildMissingShallOrMustMessage;
|
|
48
|
+
private countScenarios;
|
|
49
|
+
private formatSectionList;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1,498 @@
|
|
|
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, normalizeRequirementName, extractRequirementsSection } 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 '../../utils/file-system.js';
|
|
11
|
+
export class Validator {
|
|
12
|
+
strictMode;
|
|
13
|
+
constructor(strictMode = false) {
|
|
14
|
+
this.strictMode = strictMode;
|
|
15
|
+
}
|
|
16
|
+
async validateSpec(filePath) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
const specName = this.extractNameFromPath(filePath);
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
21
|
+
const parser = new MarkdownParser(content);
|
|
22
|
+
const spec = parser.parseSpec(specName);
|
|
23
|
+
const result = SpecSchema.safeParse(spec);
|
|
24
|
+
if (!result.success) {
|
|
25
|
+
issues.push(...this.convertZodErrors(result.error));
|
|
26
|
+
}
|
|
27
|
+
issues.push(...this.applySpecRules(spec, content));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
31
|
+
const enriched = this.enrichTopLevelError(specName, baseMessage);
|
|
32
|
+
issues.push({
|
|
33
|
+
level: 'ERROR',
|
|
34
|
+
path: 'file',
|
|
35
|
+
message: enriched,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return this.createReport(issues);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate spec content from a string (used for pre-write validation of rebuilt specs)
|
|
42
|
+
*/
|
|
43
|
+
async validateSpecContent(specName, content) {
|
|
44
|
+
const issues = [];
|
|
45
|
+
try {
|
|
46
|
+
const parser = new MarkdownParser(content);
|
|
47
|
+
const spec = parser.parseSpec(specName);
|
|
48
|
+
const result = SpecSchema.safeParse(spec);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
issues.push(...this.convertZodErrors(result.error));
|
|
51
|
+
}
|
|
52
|
+
issues.push(...this.applySpecRules(spec, content));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
56
|
+
const enriched = this.enrichTopLevelError(specName, baseMessage);
|
|
57
|
+
issues.push({ level: 'ERROR', path: 'file', message: enriched });
|
|
58
|
+
}
|
|
59
|
+
return this.createReport(issues);
|
|
60
|
+
}
|
|
61
|
+
async validateChange(filePath) {
|
|
62
|
+
const issues = [];
|
|
63
|
+
const changeName = this.extractNameFromPath(filePath);
|
|
64
|
+
try {
|
|
65
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
66
|
+
const changeDir = path.dirname(filePath);
|
|
67
|
+
const parser = new ChangeParser(content, changeDir);
|
|
68
|
+
const change = await parser.parseChangeWithDeltas(changeName);
|
|
69
|
+
const result = ChangeSchema.safeParse(change);
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
issues.push(...this.convertZodErrors(result.error));
|
|
72
|
+
}
|
|
73
|
+
issues.push(...this.applyChangeRules(change, content));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const baseMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
77
|
+
const enriched = this.enrichTopLevelError(changeName, baseMessage);
|
|
78
|
+
issues.push({
|
|
79
|
+
level: 'ERROR',
|
|
80
|
+
path: 'file',
|
|
81
|
+
message: enriched,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return this.createReport(issues);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validate delta-formatted spec files under a change directory.
|
|
88
|
+
* Enforces:
|
|
89
|
+
* - At least one delta across all files
|
|
90
|
+
* - ADDED/MODIFIED: each requirement has SHALL/MUST and at least one scenario
|
|
91
|
+
* - REMOVED: names only; no scenario/description required
|
|
92
|
+
* - RENAMED: pairs well-formed
|
|
93
|
+
* - No duplicates within sections; no cross-section conflicts per spec
|
|
94
|
+
*/
|
|
95
|
+
async validateChangeDeltaSpecs(changeDir) {
|
|
96
|
+
const issues = [];
|
|
97
|
+
const specsDir = path.join(changeDir, 'specs');
|
|
98
|
+
let totalDeltas = 0;
|
|
99
|
+
const missingHeaderSpecs = [];
|
|
100
|
+
const emptySectionSpecs = [];
|
|
101
|
+
try {
|
|
102
|
+
// Discover delta specs at any depth so the nested multi-area layout
|
|
103
|
+
// (specs/<area>/<capability>/spec.md) is validated, not just the
|
|
104
|
+
// one-level specs/<capability>/spec.md layout (#1182b). The spec-driven
|
|
105
|
+
// specs glob is specs/**/*.md; delta files are always named spec.md.
|
|
106
|
+
const specFiles = await this.findDeltaSpecFiles(specsDir);
|
|
107
|
+
for (const specFile of specFiles) {
|
|
108
|
+
let content;
|
|
109
|
+
try {
|
|
110
|
+
content = await fs.readFile(specFile, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const plan = parseDeltaSpec(content);
|
|
116
|
+
const entryPath = FileSystemUtils.toPosixPath(path.relative(specsDir, specFile));
|
|
117
|
+
// Surface (as INFO, never a failure) the non-canonical level-3 headers
|
|
118
|
+
// the delta reader skipped while parsing ADDED/MODIFIED sections —
|
|
119
|
+
// without this note a stray divider like "### Documentation
|
|
120
|
+
// Requirements" would pass validate <change> while failing
|
|
121
|
+
// archive/validate <spec>. The list comes from the parse itself, so it
|
|
122
|
+
// reflects exactly what the reader skipped.
|
|
123
|
+
for (const stray of plan.skippedHeaders) {
|
|
124
|
+
const nameless = /^requirement:?$/i.test(stray.header);
|
|
125
|
+
issues.push({
|
|
126
|
+
level: 'INFO',
|
|
127
|
+
path: entryPath,
|
|
128
|
+
line: stray.line,
|
|
129
|
+
message: nameless
|
|
130
|
+
? `Header "### ${stray.header}" in ${stray.section} is missing a requirement name and is ignored by validation. Add a name, e.g. "### Requirement: <name>".`
|
|
131
|
+
: `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.`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const sectionNames = [];
|
|
135
|
+
if (plan.sectionPresence.added)
|
|
136
|
+
sectionNames.push('## ADDED Requirements');
|
|
137
|
+
if (plan.sectionPresence.modified)
|
|
138
|
+
sectionNames.push('## MODIFIED Requirements');
|
|
139
|
+
if (plan.sectionPresence.removed)
|
|
140
|
+
sectionNames.push('## REMOVED Requirements');
|
|
141
|
+
if (plan.sectionPresence.renamed)
|
|
142
|
+
sectionNames.push('## RENAMED Requirements');
|
|
143
|
+
const hasSections = sectionNames.length > 0;
|
|
144
|
+
const hasEntries = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
|
|
145
|
+
if (!hasEntries) {
|
|
146
|
+
if (hasSections)
|
|
147
|
+
emptySectionSpecs.push({ path: entryPath, sections: sectionNames });
|
|
148
|
+
else
|
|
149
|
+
missingHeaderSpecs.push(entryPath);
|
|
150
|
+
}
|
|
151
|
+
const addedNames = new Set();
|
|
152
|
+
const modifiedNames = new Set();
|
|
153
|
+
const removedNames = new Set();
|
|
154
|
+
const renamedFrom = new Set();
|
|
155
|
+
const renamedTo = new Set();
|
|
156
|
+
// Validate ADDED
|
|
157
|
+
for (const block of plan.added) {
|
|
158
|
+
const key = normalizeRequirementName(block.name);
|
|
159
|
+
totalDeltas++;
|
|
160
|
+
if (addedNames.has(key)) {
|
|
161
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in ADDED: "${block.name}"` });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
addedNames.add(key);
|
|
165
|
+
}
|
|
166
|
+
const requirementText = this.extractRequirementText(block.raw);
|
|
167
|
+
if (!requirementText) {
|
|
168
|
+
issues.push({
|
|
169
|
+
level: 'ERROR',
|
|
170
|
+
path: entryPath,
|
|
171
|
+
message: this.containsShallOrMust(block.name)
|
|
172
|
+
? this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name)
|
|
173
|
+
: `ADDED "${block.name}" is missing requirement text`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else if (!this.containsShallOrMust(requirementText)) {
|
|
177
|
+
issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name) });
|
|
178
|
+
}
|
|
179
|
+
const scenarioCount = this.countScenarios(block.raw);
|
|
180
|
+
if (scenarioCount < 1) {
|
|
181
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario` });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Validate MODIFIED
|
|
185
|
+
for (const block of plan.modified) {
|
|
186
|
+
const key = normalizeRequirementName(block.name);
|
|
187
|
+
totalDeltas++;
|
|
188
|
+
if (modifiedNames.has(key)) {
|
|
189
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in MODIFIED: "${block.name}"` });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
modifiedNames.add(key);
|
|
193
|
+
}
|
|
194
|
+
const requirementText = this.extractRequirementText(block.raw);
|
|
195
|
+
if (!requirementText) {
|
|
196
|
+
issues.push({
|
|
197
|
+
level: 'ERROR',
|
|
198
|
+
path: entryPath,
|
|
199
|
+
message: this.containsShallOrMust(block.name)
|
|
200
|
+
? this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name)
|
|
201
|
+
: `MODIFIED "${block.name}" is missing requirement text`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else if (!this.containsShallOrMust(requirementText)) {
|
|
205
|
+
issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name) });
|
|
206
|
+
}
|
|
207
|
+
const scenarioCount = this.countScenarios(block.raw);
|
|
208
|
+
if (scenarioCount < 1) {
|
|
209
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario` });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Validate REMOVED (names only)
|
|
213
|
+
for (const name of plan.removed) {
|
|
214
|
+
const key = normalizeRequirementName(name);
|
|
215
|
+
totalDeltas++;
|
|
216
|
+
if (removedNames.has(key)) {
|
|
217
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in REMOVED: "${name}"` });
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
removedNames.add(key);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Validate RENAMED pairs
|
|
224
|
+
for (const { from, to } of plan.renamed) {
|
|
225
|
+
const fromKey = normalizeRequirementName(from);
|
|
226
|
+
const toKey = normalizeRequirementName(to);
|
|
227
|
+
totalDeltas++;
|
|
228
|
+
if (renamedFrom.has(fromKey)) {
|
|
229
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate FROM in RENAMED: "${from}"` });
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
renamedFrom.add(fromKey);
|
|
233
|
+
}
|
|
234
|
+
if (renamedTo.has(toKey)) {
|
|
235
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate TO in RENAMED: "${to}"` });
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
renamedTo.add(toKey);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Cross-section conflicts (within the same spec file)
|
|
242
|
+
for (const n of modifiedNames) {
|
|
243
|
+
if (removedNames.has(n)) {
|
|
244
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and REMOVED: "${n}"` });
|
|
245
|
+
}
|
|
246
|
+
if (addedNames.has(n)) {
|
|
247
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and ADDED: "${n}"` });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const n of addedNames) {
|
|
251
|
+
if (removedNames.has(n)) {
|
|
252
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both ADDED and REMOVED: "${n}"` });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const { from, to } of plan.renamed) {
|
|
256
|
+
const fromKey = normalizeRequirementName(from);
|
|
257
|
+
const toKey = normalizeRequirementName(to);
|
|
258
|
+
if (modifiedNames.has(fromKey)) {
|
|
259
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED references old name from RENAMED. Use new header for "${to}"` });
|
|
260
|
+
}
|
|
261
|
+
if (addedNames.has(toKey)) {
|
|
262
|
+
issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// If no specs dir, treat as no deltas
|
|
269
|
+
}
|
|
270
|
+
for (const { path: specPath, sections } of emptySectionSpecs) {
|
|
271
|
+
issues.push({
|
|
272
|
+
level: 'ERROR',
|
|
273
|
+
path: specPath,
|
|
274
|
+
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).`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
for (const path of missingHeaderSpecs) {
|
|
278
|
+
issues.push({
|
|
279
|
+
level: 'ERROR',
|
|
280
|
+
path,
|
|
281
|
+
message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (totalDeltas === 0) {
|
|
285
|
+
issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
|
|
286
|
+
}
|
|
287
|
+
return this.createReport(issues);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Recursively collect every delta `spec.md` under a change's specs directory,
|
|
291
|
+
* so both the one-level (specs/<capability>/spec.md) and nested multi-area
|
|
292
|
+
* (specs/<area>/<capability>/spec.md) layouts are discovered (#1182b).
|
|
293
|
+
* Returns absolute paths, sorted for deterministic issue ordering.
|
|
294
|
+
*/
|
|
295
|
+
async findDeltaSpecFiles(specsDir) {
|
|
296
|
+
const results = [];
|
|
297
|
+
const walk = async (dir) => {
|
|
298
|
+
let entries;
|
|
299
|
+
try {
|
|
300
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
const full = path.join(dir, entry.name);
|
|
307
|
+
if (entry.isDirectory()) {
|
|
308
|
+
await walk(full);
|
|
309
|
+
}
|
|
310
|
+
else if (entry.isFile() && entry.name === 'spec.md') {
|
|
311
|
+
results.push(full);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
await walk(specsDir);
|
|
316
|
+
return results.sort();
|
|
317
|
+
}
|
|
318
|
+
convertZodErrors(error) {
|
|
319
|
+
return error.issues.map(err => {
|
|
320
|
+
let message = err.message;
|
|
321
|
+
if (message === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
|
|
322
|
+
message = `${message}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
level: 'ERROR',
|
|
326
|
+
path: err.path.join('.'),
|
|
327
|
+
message,
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
applySpecRules(spec, content) {
|
|
332
|
+
const issues = [];
|
|
333
|
+
for (const structuralIssue of findMainSpecStructureIssues(content)) {
|
|
334
|
+
issues.push({
|
|
335
|
+
level: 'ERROR',
|
|
336
|
+
path: 'file',
|
|
337
|
+
line: structuralIssue.line,
|
|
338
|
+
message: structuralIssue.message,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (spec.overview.length < MIN_PURPOSE_LENGTH) {
|
|
342
|
+
issues.push({
|
|
343
|
+
level: 'WARNING',
|
|
344
|
+
path: 'overview',
|
|
345
|
+
message: VALIDATION_MESSAGES.PURPOSE_TOO_BRIEF,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
spec.requirements.forEach((req, index) => {
|
|
349
|
+
if (req.text.length > MAX_REQUIREMENT_TEXT_LENGTH) {
|
|
350
|
+
issues.push({
|
|
351
|
+
level: 'INFO',
|
|
352
|
+
path: `requirements[${index}]`,
|
|
353
|
+
message: VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (req.scenarios.length === 0) {
|
|
357
|
+
issues.push({
|
|
358
|
+
level: 'WARNING',
|
|
359
|
+
path: `requirements[${index}].scenarios`,
|
|
360
|
+
message: `${VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS}. ${VALIDATION_MESSAGES.GUIDE_SCENARIO_FORMAT}`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
// SHALL/MUST body-keyword enforcement for main specs (#1156). The main-spec
|
|
365
|
+
// parser collapses the requirement header into `text`, so we recover the
|
|
366
|
+
// header+body pairs here (the same source the delta path trusts) and reuse
|
|
367
|
+
// the delta detection: a body that omits the keyword errors, with the
|
|
368
|
+
// targeted "move it to the body line" hint when the keyword is in the header
|
|
369
|
+
// only and the generic message otherwise. Emitted exactly once per
|
|
370
|
+
// requirement (the Zod refine that used to emit a generic error is removed).
|
|
371
|
+
extractRequirementsSection(content).bodyBlocks.forEach((block, index) => {
|
|
372
|
+
const requirementText = this.extractRequirementText(block.raw);
|
|
373
|
+
if (!requirementText || !this.containsShallOrMust(requirementText)) {
|
|
374
|
+
issues.push({
|
|
375
|
+
level: 'ERROR',
|
|
376
|
+
path: `requirements[${index}]`,
|
|
377
|
+
message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
return issues;
|
|
382
|
+
}
|
|
383
|
+
applyChangeRules(change, content) {
|
|
384
|
+
const issues = [];
|
|
385
|
+
const MIN_DELTA_DESCRIPTION_LENGTH = 10;
|
|
386
|
+
change.deltas.forEach((delta, index) => {
|
|
387
|
+
if (!delta.description || delta.description.length < MIN_DELTA_DESCRIPTION_LENGTH) {
|
|
388
|
+
issues.push({
|
|
389
|
+
level: 'WARNING',
|
|
390
|
+
path: `deltas[${index}].description`,
|
|
391
|
+
message: VALIDATION_MESSAGES.DELTA_DESCRIPTION_TOO_BRIEF,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if ((delta.operation === 'ADDED' || delta.operation === 'MODIFIED') &&
|
|
395
|
+
(!delta.requirements || delta.requirements.length === 0)) {
|
|
396
|
+
issues.push({
|
|
397
|
+
level: 'WARNING',
|
|
398
|
+
path: `deltas[${index}].requirements`,
|
|
399
|
+
message: `${delta.operation} ${VALIDATION_MESSAGES.DELTA_MISSING_REQUIREMENTS}`,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return issues;
|
|
404
|
+
}
|
|
405
|
+
enrichTopLevelError(itemId, baseMessage) {
|
|
406
|
+
const msg = baseMessage.trim();
|
|
407
|
+
if (msg === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
|
|
408
|
+
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
|
|
409
|
+
}
|
|
410
|
+
if (msg.includes('Spec must have a Purpose section') || msg.includes('Spec must have a Requirements section')) {
|
|
411
|
+
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_SPEC_SECTIONS}`;
|
|
412
|
+
}
|
|
413
|
+
if (msg.includes('Change must have a Why section') || msg.includes('Change must have a What Changes section')) {
|
|
414
|
+
return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_CHANGE_SECTIONS}`;
|
|
415
|
+
}
|
|
416
|
+
return msg;
|
|
417
|
+
}
|
|
418
|
+
extractNameFromPath(filePath) {
|
|
419
|
+
const normalizedPath = FileSystemUtils.toPosixPath(filePath);
|
|
420
|
+
const parts = normalizedPath.split('/');
|
|
421
|
+
// Look for the directory name after 'specs' or 'changes'
|
|
422
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
423
|
+
if (parts[i] === 'specs' || parts[i] === 'changes') {
|
|
424
|
+
if (i < parts.length - 1) {
|
|
425
|
+
return parts[i + 1];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Fallback to filename without extension if not in expected structure
|
|
430
|
+
const fileName = parts[parts.length - 1] ?? '';
|
|
431
|
+
const dotIndex = fileName.lastIndexOf('.');
|
|
432
|
+
return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
|
|
433
|
+
}
|
|
434
|
+
createReport(issues) {
|
|
435
|
+
const errors = issues.filter(i => i.level === 'ERROR').length;
|
|
436
|
+
const warnings = issues.filter(i => i.level === 'WARNING').length;
|
|
437
|
+
const info = issues.filter(i => i.level === 'INFO').length;
|
|
438
|
+
const valid = this.strictMode
|
|
439
|
+
? errors === 0 && warnings === 0
|
|
440
|
+
: errors === 0;
|
|
441
|
+
return {
|
|
442
|
+
valid,
|
|
443
|
+
issues,
|
|
444
|
+
summary: {
|
|
445
|
+
errors,
|
|
446
|
+
warnings,
|
|
447
|
+
info,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
isValid(report) {
|
|
452
|
+
return report.valid;
|
|
453
|
+
}
|
|
454
|
+
extractRequirementText(blockRaw) {
|
|
455
|
+
// Delegate to the shared, fence-/metadata-/multi-line-aware body reader.
|
|
456
|
+
// Validation intentionally does not use the parser/display header-title
|
|
457
|
+
// fallback for canonical `### Requirement:` blocks: #1280 requires a
|
|
458
|
+
// SHALL/MUST that appears only in the header to receive the body-keyword
|
|
459
|
+
// hint. Line 0 is the `### Requirement: ...` header.
|
|
460
|
+
const [, ...bodyLines] = blockRaw.split('\n');
|
|
461
|
+
return extractRequirementBodyShared(bodyLines) || undefined;
|
|
462
|
+
}
|
|
463
|
+
containsShallOrMust(text) {
|
|
464
|
+
return containsShallOrMustShared(text);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Build an error message for a requirement block whose body lacks SHALL/MUST.
|
|
468
|
+
*
|
|
469
|
+
* When the SHALL/MUST keyword already appears in the requirement header (e.g.
|
|
470
|
+
* `### Requirement: The system SHALL ...`) the original generic error
|
|
471
|
+
* ("must contain SHALL or MUST") is confusing because the keyword is visibly
|
|
472
|
+
* present in the spec. Per the OpenSpec conventions the keyword has to live
|
|
473
|
+
* on the requirement body line (the line right after the header), so we point
|
|
474
|
+
* the author at that exact fix when the keyword is found in the header only.
|
|
475
|
+
*/
|
|
476
|
+
buildMissingShallOrMustMessage(prefix, blockName) {
|
|
477
|
+
const base = `${prefix} must contain SHALL or MUST`;
|
|
478
|
+
if (this.containsShallOrMust(blockName)) {
|
|
479
|
+
return `${base} in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.`;
|
|
480
|
+
}
|
|
481
|
+
return base;
|
|
482
|
+
}
|
|
483
|
+
countScenarios(blockRaw) {
|
|
484
|
+
// Fence-aware count via the shared reader: a `#### Scenario:` inside a fenced
|
|
485
|
+
// example is not a real scenario. Drop the header line (index 0).
|
|
486
|
+
return countScenariosShared(blockRaw.split('\n').slice(1));
|
|
487
|
+
}
|
|
488
|
+
formatSectionList(sections) {
|
|
489
|
+
if (sections.length === 0)
|
|
490
|
+
return '';
|
|
491
|
+
if (sections.length === 1)
|
|
492
|
+
return sections[0];
|
|
493
|
+
const head = sections.slice(0, -1);
|
|
494
|
+
const last = sections[sections.length - 1];
|
|
495
|
+
return `${head.join(', ')} and ${last}`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
//# sourceMappingURL=validator.js.map
|