@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,48 @@
|
|
|
1
|
+
export interface RequirementBlock {
|
|
2
|
+
headerLine: string;
|
|
3
|
+
name: string;
|
|
4
|
+
raw: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RequirementsSectionParts {
|
|
7
|
+
before: string;
|
|
8
|
+
headerLine: string;
|
|
9
|
+
preamble: string;
|
|
10
|
+
bodyBlocks: RequirementBlock[];
|
|
11
|
+
after: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function normalizeRequirementName(name: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractRequirementsSection(content: string): RequirementsSectionParts;
|
|
18
|
+
/**
|
|
19
|
+
* A level-3 header inside `## ADDED`/`## MODIFIED Requirements` that is not a
|
|
20
|
+
* canonical `### Requirement:` header, recorded at the moment the delta reader
|
|
21
|
+
* skips over it. Surfaced as an INFO note by `validate <change>` (#498).
|
|
22
|
+
*/
|
|
23
|
+
export interface SkippedHeader {
|
|
24
|
+
header: string;
|
|
25
|
+
section: string;
|
|
26
|
+
line: number;
|
|
27
|
+
}
|
|
28
|
+
export interface DeltaPlan {
|
|
29
|
+
added: RequirementBlock[];
|
|
30
|
+
modified: RequirementBlock[];
|
|
31
|
+
removed: string[];
|
|
32
|
+
renamed: Array<{
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
}>;
|
|
36
|
+
skippedHeaders: SkippedHeader[];
|
|
37
|
+
sectionPresence: {
|
|
38
|
+
added: boolean;
|
|
39
|
+
modified: boolean;
|
|
40
|
+
removed: boolean;
|
|
41
|
+
renamed: boolean;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseDeltaSpec(content: string): DeltaPlan;
|
|
48
|
+
//# sourceMappingURL=requirement-blocks.d.ts.map
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from './requirement-text.js';
|
|
2
|
+
export function normalizeRequirementName(name) {
|
|
3
|
+
return name.trim();
|
|
4
|
+
}
|
|
5
|
+
/** The canonical requirement header the delta reader recognizes. */
|
|
6
|
+
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
|
9
|
+
*/
|
|
10
|
+
export function extractRequirementsSection(content) {
|
|
11
|
+
const normalized = normalizeLineEndings(content);
|
|
12
|
+
const lines = normalized.split('\n');
|
|
13
|
+
const reqHeaderIndex = lines.findIndex(l => /^##\s+Requirements\s*$/i.test(l));
|
|
14
|
+
if (reqHeaderIndex === -1) {
|
|
15
|
+
// No requirements section; create an empty one at the end
|
|
16
|
+
const before = content.trimEnd();
|
|
17
|
+
const headerLine = '## Requirements';
|
|
18
|
+
return {
|
|
19
|
+
before: before ? before + '\n\n' : '',
|
|
20
|
+
headerLine,
|
|
21
|
+
preamble: '',
|
|
22
|
+
bodyBlocks: [],
|
|
23
|
+
after: '\n',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// Find end of this section: next line that starts with '## ' at same or higher level
|
|
27
|
+
let endIndex = lines.length;
|
|
28
|
+
for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
|
|
29
|
+
if (/^##\s+/.test(lines[i])) {
|
|
30
|
+
endIndex = i;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const before = lines.slice(0, reqHeaderIndex).join('\n');
|
|
35
|
+
const headerLine = lines[reqHeaderIndex];
|
|
36
|
+
const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
|
|
37
|
+
// Parse requirement blocks within section body
|
|
38
|
+
const blocks = [];
|
|
39
|
+
let cursor = 0;
|
|
40
|
+
let preambleLines = [];
|
|
41
|
+
// Collect preamble lines until first requirement header
|
|
42
|
+
while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor])) {
|
|
43
|
+
preambleLines.push(sectionBodyLines[cursor]);
|
|
44
|
+
cursor++;
|
|
45
|
+
}
|
|
46
|
+
while (cursor < sectionBodyLines.length) {
|
|
47
|
+
const headerStart = cursor;
|
|
48
|
+
const headerLineCandidate = sectionBodyLines[cursor];
|
|
49
|
+
const headerMatch = headerLineCandidate.match(REQUIREMENT_HEADER_REGEX);
|
|
50
|
+
if (!headerMatch) {
|
|
51
|
+
// Not a requirement header; skip line defensively
|
|
52
|
+
cursor++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const name = normalizeRequirementName(headerMatch[1]);
|
|
56
|
+
cursor++;
|
|
57
|
+
// Gather lines until next requirement header or end of section
|
|
58
|
+
const bodyLines = [headerLineCandidate];
|
|
59
|
+
while (cursor < sectionBodyLines.length && !REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]) && !/^##\s+/.test(sectionBodyLines[cursor])) {
|
|
60
|
+
bodyLines.push(sectionBodyLines[cursor]);
|
|
61
|
+
cursor++;
|
|
62
|
+
}
|
|
63
|
+
const raw = bodyLines.join('\n').trimEnd();
|
|
64
|
+
blocks.push({ headerLine: headerLineCandidate, name, raw });
|
|
65
|
+
}
|
|
66
|
+
const after = lines.slice(endIndex).join('\n');
|
|
67
|
+
const preamble = preambleLines.join('\n').trimEnd();
|
|
68
|
+
return {
|
|
69
|
+
before: before.trimEnd() ? before + '\n' : before,
|
|
70
|
+
headerLine,
|
|
71
|
+
preamble,
|
|
72
|
+
bodyBlocks: blocks,
|
|
73
|
+
after: after.startsWith('\n') ? after : '\n' + after,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizeLineEndings(content) {
|
|
77
|
+
return content.replace(/\r\n?/g, '\n');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
81
|
+
*/
|
|
82
|
+
export function parseDeltaSpec(content) {
|
|
83
|
+
const normalized = normalizeLineEndings(content);
|
|
84
|
+
const sections = splitTopLevelSections(normalized);
|
|
85
|
+
const addedLookup = getSectionCaseInsensitive(sections, 'ADDED Requirements');
|
|
86
|
+
const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
|
|
87
|
+
const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
|
|
88
|
+
const renamedLookup = getSectionCaseInsensitive(sections, 'RENAMED Requirements');
|
|
89
|
+
const skippedHeaders = [];
|
|
90
|
+
const added = parseRequirementBlocksFromSection(addedLookup.body, {
|
|
91
|
+
section: addedLookup.title,
|
|
92
|
+
bodyStartLine: addedLookup.bodyStartLine,
|
|
93
|
+
sink: skippedHeaders,
|
|
94
|
+
});
|
|
95
|
+
const modified = parseRequirementBlocksFromSection(modifiedLookup.body, {
|
|
96
|
+
section: modifiedLookup.title,
|
|
97
|
+
bodyStartLine: modifiedLookup.bodyStartLine,
|
|
98
|
+
sink: skippedHeaders,
|
|
99
|
+
});
|
|
100
|
+
const removedNames = parseRemovedNames(removedLookup.body);
|
|
101
|
+
const renamedPairs = parseRenamedPairs(renamedLookup.body);
|
|
102
|
+
skippedHeaders.sort((a, b) => a.line - b.line);
|
|
103
|
+
return {
|
|
104
|
+
added,
|
|
105
|
+
modified,
|
|
106
|
+
removed: removedNames,
|
|
107
|
+
renamed: renamedPairs,
|
|
108
|
+
skippedHeaders,
|
|
109
|
+
sectionPresence: {
|
|
110
|
+
added: addedLookup.found,
|
|
111
|
+
modified: modifiedLookup.found,
|
|
112
|
+
removed: removedLookup.found,
|
|
113
|
+
renamed: renamedLookup.found,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function splitTopLevelSections(content) {
|
|
118
|
+
const lines = content.split('\n');
|
|
119
|
+
const result = {};
|
|
120
|
+
const indices = [];
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
const m = lines[i].match(/^(##)\s+(.+)$/);
|
|
123
|
+
if (m) {
|
|
124
|
+
const level = m[1].length; // only care for '##'
|
|
125
|
+
indices.push({ title: m[2].trim(), index: i, level });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (let i = 0; i < indices.length; i++) {
|
|
129
|
+
const current = indices[i];
|
|
130
|
+
const next = indices[i + 1];
|
|
131
|
+
const body = lines.slice(current.index + 1, next ? next.index : lines.length).join('\n');
|
|
132
|
+
// First body line, 1-based: the header is at 0-based current.index.
|
|
133
|
+
result[current.title] = { body, bodyStartLine: current.index + 2 };
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function getSectionCaseInsensitive(sections, desired) {
|
|
138
|
+
const target = desired.toLowerCase();
|
|
139
|
+
for (const [title, { body, bodyStartLine }] of Object.entries(sections)) {
|
|
140
|
+
if (title.toLowerCase() === target)
|
|
141
|
+
return { title, body, bodyStartLine, found: true };
|
|
142
|
+
}
|
|
143
|
+
return { title: desired, body: '', bodyStartLine: 0, found: false };
|
|
144
|
+
}
|
|
145
|
+
function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
146
|
+
if (!sectionBody)
|
|
147
|
+
return [];
|
|
148
|
+
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
149
|
+
// Record the non-canonical level-3 headers this reader skips, at the moment
|
|
150
|
+
// it skips them, so the INFO note describes the reader's real boundaries.
|
|
151
|
+
// Fence-masked lines are excluded: the body reader treats them as fenced
|
|
152
|
+
// content, not as headers.
|
|
153
|
+
const fenceMask = skipped ? buildCodeFenceMask(lines) : undefined;
|
|
154
|
+
const recordIfSkippedHeader = (index) => {
|
|
155
|
+
if (!skipped || fenceMask[index])
|
|
156
|
+
return;
|
|
157
|
+
const h3 = lines[index].match(/^###\s+(.+?)\s*$/);
|
|
158
|
+
if (h3 && !REQUIREMENT_HEADER_REGEX.test(lines[index])) {
|
|
159
|
+
skipped.sink.push({
|
|
160
|
+
header: h3[1].trim(),
|
|
161
|
+
section: skipped.section,
|
|
162
|
+
line: skipped.bodyStartLine + index,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const blocks = [];
|
|
167
|
+
let i = 0;
|
|
168
|
+
while (i < lines.length) {
|
|
169
|
+
// Seek next requirement header
|
|
170
|
+
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i])) {
|
|
171
|
+
recordIfSkippedHeader(i);
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
if (i >= lines.length)
|
|
175
|
+
break;
|
|
176
|
+
const headerLine = lines[i];
|
|
177
|
+
const m = headerLine.match(REQUIREMENT_HEADER_REGEX);
|
|
178
|
+
if (!m) {
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const name = normalizeRequirementName(m[1]);
|
|
183
|
+
const buf = [headerLine];
|
|
184
|
+
i++;
|
|
185
|
+
while (i < lines.length && !REQUIREMENT_HEADER_REGEX.test(lines[i]) && !/^##\s+/.test(lines[i])) {
|
|
186
|
+
recordIfSkippedHeader(i);
|
|
187
|
+
buf.push(lines[i]);
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
blocks.push({ headerLine, name, raw: buf.join('\n').trimEnd() });
|
|
191
|
+
}
|
|
192
|
+
return blocks;
|
|
193
|
+
}
|
|
194
|
+
function parseRemovedNames(sectionBody) {
|
|
195
|
+
if (!sectionBody)
|
|
196
|
+
return [];
|
|
197
|
+
const names = [];
|
|
198
|
+
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
const m = line.match(REQUIREMENT_HEADER_REGEX);
|
|
201
|
+
if (m) {
|
|
202
|
+
names.push(normalizeRequirementName(m[1]));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Also support bullet list of headers
|
|
206
|
+
const bullet = line.match(/^\s*-\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
207
|
+
if (bullet) {
|
|
208
|
+
names.push(normalizeRequirementName(bullet[1]));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return names;
|
|
212
|
+
}
|
|
213
|
+
function parseRenamedPairs(sectionBody) {
|
|
214
|
+
if (!sectionBody)
|
|
215
|
+
return [];
|
|
216
|
+
const pairs = [];
|
|
217
|
+
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
218
|
+
let current = {};
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
221
|
+
const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
222
|
+
if (fromMatch) {
|
|
223
|
+
current.from = normalizeRequirementName(fromMatch[1]);
|
|
224
|
+
}
|
|
225
|
+
else if (toMatch) {
|
|
226
|
+
current.to = normalizeRequirementName(toMatch[1]);
|
|
227
|
+
if (current.from && current.to) {
|
|
228
|
+
pairs.push({ from: current.from, to: current.to });
|
|
229
|
+
current = {};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return pairs;
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=requirement-blocks.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, fence-aware requirement-reading helpers.
|
|
3
|
+
*
|
|
4
|
+
* The requirement reader used to be implemented twice — once for main specs
|
|
5
|
+
* (`MarkdownParser.parseRequirements`) and once for change deltas
|
|
6
|
+
* (`Validator.extractRequirementText` / `countScenarios`) — and the two drifted
|
|
7
|
+
* apart. These helpers are the single source of truth for requirement-body
|
|
8
|
+
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
|
+
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Build a per-line mask marking lines that fall inside a fenced code block
|
|
13
|
+
* (``` ``` ``` or ``` ~~~ ```), including the fence lines themselves. Mirrors the
|
|
14
|
+
* fence rules markdown uses: a fence opens on the first ```` ```/~~~ ```` of
|
|
15
|
+
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
+
* opening length, with nothing but whitespace after it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildCodeFenceMask(lines: string[]): boolean[];
|
|
19
|
+
/**
|
|
20
|
+
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
21
|
+
* as whole words so the change-delta reader and the schema-based reader accept
|
|
22
|
+
* and reject identical text.
|
|
23
|
+
*/
|
|
24
|
+
export declare function containsShallOrMust(text: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Extract the full requirement body from the lines that follow a
|
|
27
|
+
* `### Requirement:` header (the lines may include scenarios and fenced code).
|
|
28
|
+
*
|
|
29
|
+
* Captures every body line from the start up to the first header found on a
|
|
30
|
+
* non-fenced line — usually the first `#### Scenario:`, but also a stray `###`
|
|
31
|
+
* divider the delta reader absorbed into the block — skipping blank lines and
|
|
32
|
+
* any line inside a fenced code block. `**metadata**:` lines are skipped only
|
|
33
|
+
* when other body text remains: a requirement written entirely as
|
|
34
|
+
* `**Constraint**: The system MUST ...` keeps that line as its body. Captured
|
|
35
|
+
* lines are trimmed and joined with newlines so a requirement whose text wraps
|
|
36
|
+
* across lines — or whose `SHALL`/`MUST` lands on a later line — is read in
|
|
37
|
+
* full.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractRequirementBody(bodyLines: string[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* Parser/display fallback for a requirement block with no body text. This is
|
|
42
|
+
* what lets a bare `### The system SHALL ...` header remain readable on the
|
|
43
|
+
* spec path (the title is the requirement). Validator body-keyword checks for
|
|
44
|
+
* canonical `### Requirement:` blocks use `extractRequirementBody` directly so
|
|
45
|
+
* a keyword that appears only in the header still receives the #1156/#1280
|
|
46
|
+
* body-keyword hint.
|
|
47
|
+
*/
|
|
48
|
+
export declare function extractRequirementText(headerTitle: string, bodyLines: string[]): string;
|
|
49
|
+
/**
|
|
50
|
+
* Count the real scenarios in a requirement block: `#### ` headers on non-fenced
|
|
51
|
+
* lines. A `#### Scenario:` that lives inside a fenced example is not a real
|
|
52
|
+
* scenario and is not counted.
|
|
53
|
+
*/
|
|
54
|
+
export declare function countScenarios(bodyLines: string[]): number;
|
|
55
|
+
//# sourceMappingURL=requirement-text.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, fence-aware requirement-reading helpers.
|
|
3
|
+
*
|
|
4
|
+
* The requirement reader used to be implemented twice — once for main specs
|
|
5
|
+
* (`MarkdownParser.parseRequirements`) and once for change deltas
|
|
6
|
+
* (`Validator.extractRequirementText` / `countScenarios`) — and the two drifted
|
|
7
|
+
* apart. These helpers are the single source of truth for requirement-body
|
|
8
|
+
* extraction, scenario counting, and `SHALL`/`MUST` detection in
|
|
9
|
+
* `validate <change>`, `validate <spec>`, and `archive`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Build a per-line mask marking lines that fall inside a fenced code block
|
|
13
|
+
* (``` ``` ``` or ``` ~~~ ```), including the fence lines themselves. Mirrors the
|
|
14
|
+
* fence rules markdown uses: a fence opens on the first ```` ```/~~~ ```` of
|
|
15
|
+
* length >= 3 and closes on a line of the same marker whose length is >= the
|
|
16
|
+
* opening length, with nothing but whitespace after it.
|
|
17
|
+
*/
|
|
18
|
+
export function buildCodeFenceMask(lines) {
|
|
19
|
+
const mask = new Array(lines.length).fill(false);
|
|
20
|
+
let activeFence = null;
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const fence = getFenceMarker(lines[i]);
|
|
23
|
+
if (!activeFence) {
|
|
24
|
+
if (fence) {
|
|
25
|
+
activeFence = fence;
|
|
26
|
+
mask[i] = true;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
mask[i] = true;
|
|
31
|
+
if (isClosingFence(lines[i], activeFence)) {
|
|
32
|
+
activeFence = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return mask;
|
|
36
|
+
}
|
|
37
|
+
function getFenceMarker(line) {
|
|
38
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
|
|
39
|
+
if (!fenceMatch) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
marker: fenceMatch[1][0],
|
|
44
|
+
length: fenceMatch[1].length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function isClosingFence(line, activeFence) {
|
|
48
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
49
|
+
return Boolean(fenceMatch &&
|
|
50
|
+
fenceMatch[1][0] === activeFence.marker &&
|
|
51
|
+
fenceMatch[1].length >= activeFence.length);
|
|
52
|
+
}
|
|
53
|
+
/** Lines that look like `**ID**: ...` / `**Priority**: ...` metadata. */
|
|
54
|
+
const METADATA_LINE = /^\*\*[^*]+\*\*:/;
|
|
55
|
+
/** Any markdown header line — the boundary where a requirement body ends. */
|
|
56
|
+
const HEADER_LINE = /^#{1,6}\s/;
|
|
57
|
+
/**
|
|
58
|
+
* A level-4 header. Deliberately matches ANY `####` header, not only
|
|
59
|
+
* `#### Scenario:` — the spec path treats every level-4 child of a requirement
|
|
60
|
+
* as a scenario, so the delta counter must too (parity). Don't tighten this to
|
|
61
|
+
* `Scenario:` without changing both paths together.
|
|
62
|
+
*/
|
|
63
|
+
const SCENARIO_HEADER = /^####\s+/;
|
|
64
|
+
/**
|
|
65
|
+
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
66
|
+
* as whole words so the change-delta reader and the schema-based reader accept
|
|
67
|
+
* and reject identical text.
|
|
68
|
+
*/
|
|
69
|
+
export function containsShallOrMust(text) {
|
|
70
|
+
return /\b(SHALL|MUST)\b/.test(text);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract the full requirement body from the lines that follow a
|
|
74
|
+
* `### Requirement:` header (the lines may include scenarios and fenced code).
|
|
75
|
+
*
|
|
76
|
+
* Captures every body line from the start up to the first header found on a
|
|
77
|
+
* non-fenced line — usually the first `#### Scenario:`, but also a stray `###`
|
|
78
|
+
* divider the delta reader absorbed into the block — skipping blank lines and
|
|
79
|
+
* any line inside a fenced code block. `**metadata**:` lines are skipped only
|
|
80
|
+
* when other body text remains: a requirement written entirely as
|
|
81
|
+
* `**Constraint**: The system MUST ...` keeps that line as its body. Captured
|
|
82
|
+
* lines are trimmed and joined with newlines so a requirement whose text wraps
|
|
83
|
+
* across lines — or whose `SHALL`/`MUST` lands on a later line — is read in
|
|
84
|
+
* full.
|
|
85
|
+
*/
|
|
86
|
+
export function extractRequirementBody(bodyLines) {
|
|
87
|
+
const mask = buildCodeFenceMask(bodyLines);
|
|
88
|
+
const captured = [];
|
|
89
|
+
const metadata = [];
|
|
90
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
91
|
+
if (mask[i])
|
|
92
|
+
continue; // inside a fenced code block
|
|
93
|
+
const line = bodyLines[i];
|
|
94
|
+
if (HEADER_LINE.test(line))
|
|
95
|
+
break; // first scenario or stray divider
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (trimmed.length === 0)
|
|
98
|
+
continue; // blank
|
|
99
|
+
if (METADATA_LINE.test(trimmed)) {
|
|
100
|
+
metadata.push(trimmed); // **ID**: / **Priority**: ...
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
captured.push(trimmed);
|
|
104
|
+
}
|
|
105
|
+
if (captured.length > 0)
|
|
106
|
+
return captured.join('\n');
|
|
107
|
+
return metadata.join('\n'); // metadata-only body: the metadata IS the body
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parser/display fallback for a requirement block with no body text. This is
|
|
111
|
+
* what lets a bare `### The system SHALL ...` header remain readable on the
|
|
112
|
+
* spec path (the title is the requirement). Validator body-keyword checks for
|
|
113
|
+
* canonical `### Requirement:` blocks use `extractRequirementBody` directly so
|
|
114
|
+
* a keyword that appears only in the header still receives the #1156/#1280
|
|
115
|
+
* body-keyword hint.
|
|
116
|
+
*/
|
|
117
|
+
export function extractRequirementText(headerTitle, bodyLines) {
|
|
118
|
+
return extractRequirementBody(bodyLines) || headerTitle.trim();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Count the real scenarios in a requirement block: `#### ` headers on non-fenced
|
|
122
|
+
* lines. A `#### Scenario:` that lives inside a fenced example is not a real
|
|
123
|
+
* scenario and is not counted.
|
|
124
|
+
*/
|
|
125
|
+
export function countScenarios(bodyLines) {
|
|
126
|
+
const mask = buildCodeFenceMask(bodyLines);
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
129
|
+
if (mask[i])
|
|
130
|
+
continue;
|
|
131
|
+
if (SCENARIO_HEADER.test(bodyLines[i]))
|
|
132
|
+
count++;
|
|
133
|
+
}
|
|
134
|
+
return count;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=requirement-text.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface MainSpecStructureIssue {
|
|
2
|
+
kind: 'delta-header' | 'requirement-outside-requirements';
|
|
3
|
+
line: number;
|
|
4
|
+
header: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function findMainSpecStructureIssues(content: string): MainSpecStructureIssue[];
|
|
8
|
+
export declare function stripFencedCodeBlocksPreservingLines(content: string): string;
|
|
9
|
+
//# sourceMappingURL=spec-structure.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
2
|
+
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
3
|
+
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
4
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/i;
|
|
5
|
+
export function findMainSpecStructureIssues(content) {
|
|
6
|
+
const normalized = content.replace(/\r\n?/g, '\n');
|
|
7
|
+
const stripped = stripFencedCodeBlocksPreservingLines(normalized);
|
|
8
|
+
const lines = stripped.split('\n');
|
|
9
|
+
const issues = [];
|
|
10
|
+
const requirementsHeaderIndex = lines.findIndex(line => REQUIREMENTS_SECTION_HEADER.test(line));
|
|
11
|
+
let requirementsEndIndex = lines.length;
|
|
12
|
+
if (requirementsHeaderIndex !== -1) {
|
|
13
|
+
for (let i = requirementsHeaderIndex + 1; i < lines.length; i++) {
|
|
14
|
+
if (TOP_LEVEL_SECTION_HEADER.test(lines[i])) {
|
|
15
|
+
requirementsEndIndex = i;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (DELTA_HEADER.test(line)) {
|
|
27
|
+
issues.push({
|
|
28
|
+
kind: 'delta-header',
|
|
29
|
+
line: i + 1,
|
|
30
|
+
header: trimmed,
|
|
31
|
+
message: `Main spec contains delta header "${trimmed}". ` +
|
|
32
|
+
'Delta headers are only valid inside openspec/changes/<name>/specs/<capability>/spec.md ' +
|
|
33
|
+
'and truncate the parsed ## Requirements section.',
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const requirementMatch = line.match(REQUIREMENT_HEADER);
|
|
38
|
+
if (!requirementMatch) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const insideRequirements = requirementsHeaderIndex !== -1 &&
|
|
42
|
+
i > requirementsHeaderIndex &&
|
|
43
|
+
i < requirementsEndIndex;
|
|
44
|
+
if (!insideRequirements) {
|
|
45
|
+
issues.push({
|
|
46
|
+
kind: 'requirement-outside-requirements',
|
|
47
|
+
line: i + 1,
|
|
48
|
+
header: trimmed,
|
|
49
|
+
message: `Requirement header "${trimmed}" appears outside the main ## Requirements section. ` +
|
|
50
|
+
'Main specs only parse requirements inside that section, so this requirement is currently invisible to validate, list, and archive.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
export function stripFencedCodeBlocksPreservingLines(content) {
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
const output = [];
|
|
59
|
+
let activeFence = null;
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
62
|
+
if (!activeFence) {
|
|
63
|
+
if (fenceMatch) {
|
|
64
|
+
activeFence = {
|
|
65
|
+
marker: fenceMatch[1][0],
|
|
66
|
+
length: fenceMatch[1].length,
|
|
67
|
+
};
|
|
68
|
+
output.push('');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
output.push(line);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
output.push('');
|
|
76
|
+
if (isClosingFence(line, activeFence)) {
|
|
77
|
+
activeFence = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return output.join('\n');
|
|
81
|
+
}
|
|
82
|
+
function isClosingFence(line, activeFence) {
|
|
83
|
+
const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
84
|
+
return Boolean(fenceMatch &&
|
|
85
|
+
fenceMatch[1][0] === activeFence.marker &&
|
|
86
|
+
fenceMatch[1].length >= activeFence.length);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=spec-structure.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type PlanningHomeKind = 'repo';
|
|
2
|
+
export interface PlanningHome {
|
|
3
|
+
kind: PlanningHomeKind;
|
|
4
|
+
root: string;
|
|
5
|
+
changesDir: string;
|
|
6
|
+
defaultSchema: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ResolvePlanningHomeOptions {
|
|
9
|
+
startPath?: string;
|
|
10
|
+
allowImplicitRepoRoot?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function findRepoPlanningRootSync(startPath?: string): string | null;
|
|
13
|
+
export declare function resolveCurrentPlanningHomeSync(options?: ResolvePlanningHomeOptions): PlanningHome;
|
|
14
|
+
export declare function getChangeDir(planningHome: PlanningHome, changeName: string): string;
|
|
15
|
+
export declare function formatChangeLocation(planningHome: PlanningHome, changeName: string): string;
|
|
16
|
+
//# sourceMappingURL=planning-home.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
4
|
+
const REPO_DEFAULT_SCHEMA = 'spec-driven';
|
|
5
|
+
function pathExistsAsDirectory(candidatePath) {
|
|
6
|
+
try {
|
|
7
|
+
return fs.statSync(candidatePath).isDirectory();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function getSearchStartDirectory(startPath) {
|
|
14
|
+
const resolved = path.resolve(startPath);
|
|
15
|
+
try {
|
|
16
|
+
const stats = fs.statSync(resolved);
|
|
17
|
+
const searchStart = stats.isDirectory() ? resolved : path.dirname(resolved);
|
|
18
|
+
return FileSystemUtils.canonicalizeExistingPath(searchStart);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function findNearestAncestor(startPath, predicate) {
|
|
25
|
+
let currentDir = getSearchStartDirectory(startPath);
|
|
26
|
+
while (true) {
|
|
27
|
+
if (predicate(currentDir)) {
|
|
28
|
+
return FileSystemUtils.canonicalizeExistingPath(currentDir);
|
|
29
|
+
}
|
|
30
|
+
const parentDir = path.dirname(currentDir);
|
|
31
|
+
if (parentDir === currentDir) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
currentDir = parentDir;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function findRepoPlanningRootSync(startPath = process.cwd()) {
|
|
38
|
+
return findNearestAncestor(startPath, (dirPath) => pathExistsAsDirectory(path.join(dirPath, 'openspec')));
|
|
39
|
+
}
|
|
40
|
+
function repoPlanningHome(repoRoot) {
|
|
41
|
+
return {
|
|
42
|
+
kind: 'repo',
|
|
43
|
+
root: repoRoot,
|
|
44
|
+
changesDir: path.join(repoRoot, 'openspec', 'changes'),
|
|
45
|
+
defaultSchema: REPO_DEFAULT_SCHEMA,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function resolveCurrentPlanningHomeSync(options = {}) {
|
|
49
|
+
const startPath = options.startPath ?? process.cwd();
|
|
50
|
+
const searchStart = getSearchStartDirectory(startPath);
|
|
51
|
+
const repoRoot = findRepoPlanningRootSync(searchStart);
|
|
52
|
+
if (repoRoot) {
|
|
53
|
+
return repoPlanningHome(repoRoot);
|
|
54
|
+
}
|
|
55
|
+
if (options.allowImplicitRepoRoot === false) {
|
|
56
|
+
throw new Error('No OpenSpec planning home found from the current directory.');
|
|
57
|
+
}
|
|
58
|
+
return repoPlanningHome(FileSystemUtils.canonicalizeExistingPath(searchStart));
|
|
59
|
+
}
|
|
60
|
+
export function getChangeDir(planningHome, changeName) {
|
|
61
|
+
return FileSystemUtils.joinPath(planningHome.changesDir, changeName);
|
|
62
|
+
}
|
|
63
|
+
export function formatChangeLocation(planningHome, changeName) {
|
|
64
|
+
// Repo homes always nest changesDir under the root.
|
|
65
|
+
return path.relative(planningHome.root, getChangeDir(planningHome, changeName));
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=planning-home.js.map
|