@codewalla_india/openspec 1.3.1 → 1.3.3
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 -22
- package/README.md +214 -133
- package/bin/openspec.js +5 -5
- package/dist/cli/index.js +99 -62
- package/dist/commands/change.d.ts +0 -2
- package/dist/commands/change.js +107 -86
- package/dist/commands/config.js +14 -16
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +79 -30
- package/dist/commands/modify.d.ts +26 -0
- package/dist/commands/modify.js +147 -0
- package/dist/commands/schema.js +170 -91
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/store.js +15 -21
- package/dist/commands/validate.d.ts +8 -0
- package/dist/commands/validate.js +43 -20
- package/dist/commands/workflow/index.d.ts +2 -2
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +6 -26
- package/dist/commands/workflow/instructions.js +183 -376
- package/dist/commands/workflow/new-change.d.ts +0 -4
- package/dist/commands/workflow/new-change.js +10 -28
- package/dist/commands/workflow/shared.d.ts +34 -19
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +10 -32
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/commands/workset.d.ts +2 -2
- package/dist/commands/workset.js +19 -22
- package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
- package/dist/comprehension-quiz/fingerprint.js +84 -0
- package/dist/comprehension-quiz/index.d.ts +42 -0
- package/dist/comprehension-quiz/index.js +47 -0
- package/dist/comprehension-quiz/pass-record.d.ts +44 -0
- package/dist/comprehension-quiz/pass-record.js +98 -0
- package/dist/comprehension-quiz/providers/anthropic.d.ts +35 -0
- package/dist/comprehension-quiz/providers/anthropic.js +69 -0
- package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
- package/dist/comprehension-quiz/providers/factory.js +174 -0
- package/dist/comprehension-quiz/providers/interface.d.ts +98 -0
- package/dist/comprehension-quiz/providers/interface.js +150 -0
- package/dist/comprehension-quiz/providers/local.d.ts +35 -0
- package/dist/comprehension-quiz/providers/local.js +69 -0
- package/dist/comprehension-quiz/providers/ollama.d.ts +35 -0
- package/dist/comprehension-quiz/providers/ollama.js +69 -0
- package/dist/comprehension-quiz/providers/openai.d.ts +35 -0
- package/dist/comprehension-quiz/providers/openai.js +72 -0
- package/dist/comprehension-quiz/question-generator.d.ts +117 -0
- package/dist/comprehension-quiz/question-generator.js +203 -0
- package/dist/comprehension-quiz/quiz-executor.d.ts +75 -0
- package/dist/comprehension-quiz/quiz-executor.js +181 -0
- package/dist/comprehension-quiz/types.d.ts +137 -0
- package/dist/comprehension-quiz/types.js +11 -0
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1350 -190
- package/dist/core/artifact-graph/graph.d.ts +28 -9
- package/dist/core/artifact-graph/graph.js +94 -43
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +10 -1
- package/dist/core/change-status-policy.js +30 -2
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +11 -5
- package/dist/core/command-generation/adapters/antigravity.js +6 -5
- package/dist/core/command-generation/adapters/auggie.js +7 -6
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +11 -10
- package/dist/core/command-generation/adapters/claude.d.ts +1 -1
- package/dist/core/command-generation/adapters/claude.js +12 -17
- package/dist/core/command-generation/adapters/cline.js +5 -5
- package/dist/core/command-generation/adapters/codebuddy.js +8 -7
- package/dist/core/command-generation/adapters/continue.js +8 -7
- package/dist/core/command-generation/adapters/costrict.js +7 -6
- package/dist/core/command-generation/adapters/crush.js +9 -9
- package/dist/core/command-generation/adapters/cursor.js +8 -8
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +7 -6
- package/dist/core/command-generation/adapters/gemini.js +40 -5
- package/dist/core/command-generation/adapters/github-copilot.js +6 -5
- package/dist/core/command-generation/adapters/iflow.js +9 -8
- package/dist/core/command-generation/adapters/index.d.ts +4 -2
- package/dist/core/command-generation/adapters/index.js +4 -2
- package/dist/core/command-generation/adapters/junie.js +6 -5
- package/dist/core/command-generation/adapters/kilocode.js +1 -1
- package/dist/core/command-generation/adapters/kiro.js +6 -5
- package/dist/core/command-generation/adapters/lingma.js +9 -9
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.js +6 -8
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +7 -10
- package/dist/core/command-generation/adapters/qoder.js +9 -9
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +14 -9
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +9 -9
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +8 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +16 -61
- package/dist/core/completions/generators/bash-generator.js +41 -41
- package/dist/core/completions/generators/fish-generator.js +7 -7
- package/dist/core/completions/generators/powershell-generator.js +44 -37
- package/dist/core/completions/generators/zsh-generator.js +33 -33
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/completions/templates/bash-templates.js +24 -24
- package/dist/core/completions/templates/fish-templates.js +38 -38
- package/dist/core/completions/templates/powershell-templates.js +28 -28
- package/dist/core/completions/templates/zsh-templates.js +39 -39
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +46 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +474 -73
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +23 -16
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- package/dist/core/modify/conflict-detector.d.ts +26 -0
- package/dist/core/modify/conflict-detector.js +62 -0
- package/dist/core/modify/dependency-propagator.d.ts +38 -0
- package/dist/core/modify/dependency-propagator.js +103 -0
- package/dist/core/modify/history-tracker.d.ts +34 -0
- package/dist/core/modify/history-tracker.js +114 -0
- package/dist/core/modify/index.d.ts +7 -0
- package/dist/core/modify/index.js +7 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.js +18 -15
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +35 -21
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.d.ts +0 -3
- package/dist/core/parsers/markdown-parser.js +7 -60
- package/dist/core/parsers/requirement-blocks.d.ts +29 -0
- package/dist/core/parsers/requirement-blocks.js +150 -31
- package/dist/core/parsers/requirement-text.d.ts +48 -0
- package/dist/core/parsers/requirement-text.js +98 -0
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +49 -50
- package/dist/core/profiles.d.ts +2 -2
- package/dist/core/profiles.js +2 -2
- package/dist/core/project-config.d.ts +27 -18
- package/dist/core/project-config.js +101 -34
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/schemas/base.schema.js +7 -2
- 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 +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-generation.js +17 -13
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +27 -7
- package/dist/core/shared/tool-detection.js +167 -37
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +667 -145
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/store/operations.js +20 -0
- package/dist/core/templates/skill-templates.d.ts +3 -2
- package/dist/core/templates/skill-templates.js +3 -2
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +186 -224
- package/dist/core/templates/workflows/archive-change.js +387 -260
- package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
- package/dist/core/templates/workflows/continue-change.js +210 -219
- package/dist/core/templates/workflows/explore.js +479 -443
- package/dist/core/templates/workflows/feedback.js +97 -97
- package/dist/core/templates/workflows/ff-change.js +200 -191
- package/dist/core/templates/workflows/modify-change.d.ts +4 -1
- package/dist/core/templates/workflows/modify-change.js +193 -71
- package/dist/core/templates/workflows/new-change.js +127 -134
- package/dist/core/templates/workflows/onboard.js +548 -542
- package/dist/core/templates/workflows/propose.js +273 -217
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +500 -273
- package/dist/core/templates/workflows/update-change.d.ts +10 -0
- package/dist/core/templates/workflows/update-change.js +177 -0
- package/dist/core/templates/workflows/verify-change.js +324 -326
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +411 -96
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/plan-validator.d.ts +8 -0
- package/dist/core/validation/plan-validator.js +45 -0
- package/dist/core/validation/task-numbering.d.ts +16 -0
- package/dist/core/validation/task-numbering.js +57 -0
- package/dist/core/validation/validator.d.ts +27 -3
- package/dist/core/validation/validator.js +337 -48
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +13 -18
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/caller-detection.d.ts +27 -0
- package/dist/telemetry/caller-detection.js +85 -0
- package/dist/telemetry/caller-detection.test.d.ts +5 -0
- package/dist/telemetry/config.d.ts +48 -2
- package/dist/telemetry/config.js +113 -11
- package/dist/telemetry/config.test.d.ts +5 -0
- package/dist/telemetry/deduplication.d.ts +35 -0
- package/dist/telemetry/deduplication.js +82 -0
- package/dist/telemetry/deduplication.test.d.ts +5 -0
- package/dist/telemetry/identity.d.ts +62 -22
- package/dist/telemetry/identity.js +147 -102
- package/dist/telemetry/index.d.ts +126 -15
- package/dist/telemetry/index.js +569 -37
- package/dist/telemetry/sanitization.d.ts +40 -0
- package/dist/telemetry/sanitization.js +114 -0
- package/dist/telemetry/sanitization.test.d.ts +5 -0
- package/dist/telemetry/throttling.d.ts +20 -0
- package/dist/telemetry/throttling.js +70 -0
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +115 -0
- package/dist/utils/task-progress.d.ts +29 -1
- package/dist/utils/task-progress.js +109 -15
- package/package.json +96 -84
- package/schemas/spec-driven/schema.yaml +252 -184
- package/schemas/spec-driven/templates/design.md +19 -19
- package/schemas/spec-driven/templates/plan-template.md +91 -0
- package/schemas/spec-driven/templates/proposal.md +29 -24
- package/schemas/spec-driven/templates/spec.md +11 -8
- package/schemas/spec-driven/templates/tasks.md +9 -9
- package/scripts/postinstall.js +83 -83
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
- package/dist/core/comprehension/config.d.ts +0 -20
- package/dist/core/comprehension/config.js +0 -23
- package/dist/core/comprehension/fingerprint.d.ts +0 -14
- package/dist/core/comprehension/fingerprint.js +0 -33
- package/dist/core/comprehension/index.d.ts +0 -57
- package/dist/core/comprehension/index.js +0 -87
- package/dist/core/comprehension/pass-record.d.ts +0 -29
- package/dist/core/comprehension/pass-record.js +0 -64
- package/dist/core/comprehension/stats.d.ts +0 -35
- package/dist/core/comprehension/stats.js +0 -133
- package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
- package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
- package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
- package/dist/core/templates/workflows/mcp-guidance.js +0 -131
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
- package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
- package/dist/telemetry/caller.d.ts +0 -5
- package/dist/telemetry/caller.js +0 -29
- package/dist/telemetry/client.d.ts +0 -27
- package/dist/telemetry/client.js +0 -127
- package/dist/telemetry/command-context.d.ts +0 -13
- package/dist/telemetry/command-context.js +0 -59
- package/dist/telemetry/comprehension.d.ts +0 -44
- package/dist/telemetry/comprehension.js +0 -105
- package/dist/telemetry/content.d.ts +0 -10
- package/dist/telemetry/content.js +0 -56
- package/dist/telemetry/git-stats.d.ts +0 -12
- package/dist/telemetry/git-stats.js +0 -69
- package/dist/telemetry/identify-cache.d.ts +0 -7
- package/dist/telemetry/identify-cache.js +0 -47
- package/dist/telemetry/input.d.ts +0 -17
- package/dist/telemetry/input.js +0 -68
- package/dist/telemetry/marker.d.ts +0 -37
- package/dist/telemetry/marker.js +0 -67
- package/dist/telemetry/workflow.d.ts +0 -85
- package/dist/telemetry/workflow.js +0 -318
- package/schemas/spec-driven/templates/plan.md +0 -19
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from './requirement-text.js';
|
|
1
2
|
export function normalizeRequirementName(name) {
|
|
2
3
|
return name.trim();
|
|
3
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Case- and whitespace-insensitive fold of a requirement name. Requirement
|
|
7
|
+
* matching itself is case-sensitive (normalizeRequirementName); this fold
|
|
8
|
+
* exists only for typo detection - near-miss REMOVED headers and the
|
|
9
|
+
* RENAMED+REMOVED cross-section conflict - where two spellings that differ
|
|
10
|
+
* only in case or interior whitespace mean a mistake, never two requirements.
|
|
11
|
+
*/
|
|
12
|
+
export function foldRequirementName(name) {
|
|
13
|
+
return normalizeRequirementName(name).toLowerCase().replace(/\s+/g, ' ');
|
|
14
|
+
}
|
|
15
|
+
/** The canonical requirement header the delta reader recognizes. */
|
|
4
16
|
const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
5
17
|
/**
|
|
6
18
|
* Extracts the Requirements section from a spec file and parses requirement blocks.
|
|
@@ -8,7 +20,8 @@ const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
|
|
|
8
20
|
export function extractRequirementsSection(content) {
|
|
9
21
|
const normalized = normalizeLineEndings(content);
|
|
10
22
|
const lines = normalized.split('\n');
|
|
11
|
-
const
|
|
23
|
+
const fenceMask = buildCodeFenceMask(lines);
|
|
24
|
+
const reqHeaderIndex = lines.findIndex((l, i) => !fenceMask[i] && /^##\s+Requirements\s*$/i.test(l));
|
|
12
25
|
if (reqHeaderIndex === -1) {
|
|
13
26
|
// No requirements section; create an empty one at the end
|
|
14
27
|
const before = content.trimEnd();
|
|
@@ -24,7 +37,7 @@ export function extractRequirementsSection(content) {
|
|
|
24
37
|
// Find end of this section: next line that starts with '## ' at same or higher level
|
|
25
38
|
let endIndex = lines.length;
|
|
26
39
|
for (let i = reqHeaderIndex + 1; i < lines.length; i++) {
|
|
27
|
-
if (/^##\s+/.test(lines[i])) {
|
|
40
|
+
if (!fenceMask[i] && /^##\s+/.test(lines[i])) {
|
|
28
41
|
endIndex = i;
|
|
29
42
|
break;
|
|
30
43
|
}
|
|
@@ -32,29 +45,31 @@ export function extractRequirementsSection(content) {
|
|
|
32
45
|
const before = lines.slice(0, reqHeaderIndex).join('\n');
|
|
33
46
|
const headerLine = lines[reqHeaderIndex];
|
|
34
47
|
const sectionBodyLines = lines.slice(reqHeaderIndex + 1, endIndex);
|
|
48
|
+
const sectionBodyMask = fenceMask.slice(reqHeaderIndex + 1, endIndex);
|
|
49
|
+
const isRequirementHeader = (cursor) => !sectionBodyMask[cursor] && REQUIREMENT_HEADER_REGEX.test(sectionBodyLines[cursor]);
|
|
50
|
+
const isTopLevelHeader = (cursor) => !sectionBodyMask[cursor] && /^##\s+/.test(sectionBodyLines[cursor]);
|
|
35
51
|
// Parse requirement blocks within section body
|
|
36
52
|
const blocks = [];
|
|
37
53
|
let cursor = 0;
|
|
38
54
|
let preambleLines = [];
|
|
39
55
|
// Collect preamble lines until first requirement header
|
|
40
|
-
while (cursor < sectionBodyLines.length && !
|
|
56
|
+
while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor)) {
|
|
41
57
|
preambleLines.push(sectionBodyLines[cursor]);
|
|
42
58
|
cursor++;
|
|
43
59
|
}
|
|
44
60
|
while (cursor < sectionBodyLines.length) {
|
|
45
|
-
const headerStart = cursor;
|
|
46
61
|
const headerLineCandidate = sectionBodyLines[cursor];
|
|
47
|
-
|
|
48
|
-
if (!headerMatch) {
|
|
62
|
+
if (!isRequirementHeader(cursor)) {
|
|
49
63
|
// Not a requirement header; skip line defensively
|
|
50
64
|
cursor++;
|
|
51
65
|
continue;
|
|
52
66
|
}
|
|
67
|
+
const headerMatch = headerLineCandidate.match(REQUIREMENT_HEADER_REGEX);
|
|
53
68
|
const name = normalizeRequirementName(headerMatch[1]);
|
|
54
69
|
cursor++;
|
|
55
70
|
// Gather lines until next requirement header or end of section
|
|
56
71
|
const bodyLines = [headerLineCandidate];
|
|
57
|
-
while (cursor < sectionBodyLines.length && !
|
|
72
|
+
while (cursor < sectionBodyLines.length && !isRequirementHeader(cursor) && !isTopLevelHeader(cursor)) {
|
|
58
73
|
bodyLines.push(sectionBodyLines[cursor]);
|
|
59
74
|
cursor++;
|
|
60
75
|
}
|
|
@@ -72,27 +87,42 @@ export function extractRequirementsSection(content) {
|
|
|
72
87
|
};
|
|
73
88
|
}
|
|
74
89
|
function normalizeLineEndings(content) {
|
|
75
|
-
|
|
90
|
+
// Strip a UTF-8 BOM: Windows editors and PowerShell redirects prepend one,
|
|
91
|
+
// and it would keep the first line's `## ADDED Requirements` from matching.
|
|
92
|
+
return content.replace(/^/, '').replace(/\r\n?/g, '\n');
|
|
76
93
|
}
|
|
77
94
|
/**
|
|
78
95
|
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
|
|
79
96
|
*/
|
|
80
97
|
export function parseDeltaSpec(content) {
|
|
81
98
|
const normalized = normalizeLineEndings(content);
|
|
82
|
-
const
|
|
99
|
+
const lines = normalized.split('\n');
|
|
100
|
+
const fenceMask = buildCodeFenceMask(lines);
|
|
101
|
+
const sections = splitTopLevelSections(lines, fenceMask);
|
|
83
102
|
const addedLookup = getSectionCaseInsensitive(sections, 'ADDED Requirements');
|
|
84
103
|
const modifiedLookup = getSectionCaseInsensitive(sections, 'MODIFIED Requirements');
|
|
85
104
|
const removedLookup = getSectionCaseInsensitive(sections, 'REMOVED Requirements');
|
|
86
105
|
const renamedLookup = getSectionCaseInsensitive(sections, 'RENAMED Requirements');
|
|
87
|
-
const
|
|
88
|
-
const
|
|
106
|
+
const skippedHeaders = [];
|
|
107
|
+
const added = parseRequirementBlocksFromSection(addedLookup.body, {
|
|
108
|
+
section: addedLookup.title,
|
|
109
|
+
bodyStartLine: addedLookup.bodyStartLine,
|
|
110
|
+
sink: skippedHeaders,
|
|
111
|
+
});
|
|
112
|
+
const modified = parseRequirementBlocksFromSection(modifiedLookup.body, {
|
|
113
|
+
section: modifiedLookup.title,
|
|
114
|
+
bodyStartLine: modifiedLookup.bodyStartLine,
|
|
115
|
+
sink: skippedHeaders,
|
|
116
|
+
});
|
|
89
117
|
const removedNames = parseRemovedNames(removedLookup.body);
|
|
90
118
|
const renamedPairs = parseRenamedPairs(renamedLookup.body);
|
|
119
|
+
skippedHeaders.sort((a, b) => a.line - b.line);
|
|
91
120
|
return {
|
|
92
121
|
added,
|
|
93
122
|
modified,
|
|
94
123
|
removed: removedNames,
|
|
95
124
|
renamed: renamedPairs,
|
|
125
|
+
skippedHeaders,
|
|
96
126
|
sectionPresence: {
|
|
97
127
|
added: addedLookup.found,
|
|
98
128
|
modified: modifiedLookup.found,
|
|
@@ -101,43 +131,65 @@ export function parseDeltaSpec(content) {
|
|
|
101
131
|
},
|
|
102
132
|
};
|
|
103
133
|
}
|
|
104
|
-
function splitTopLevelSections(
|
|
105
|
-
const lines = content.split('\n');
|
|
134
|
+
function splitTopLevelSections(lines, fenceMask) {
|
|
106
135
|
const result = {};
|
|
107
136
|
const indices = [];
|
|
108
137
|
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
if (fenceMask[i])
|
|
139
|
+
continue;
|
|
109
140
|
const m = lines[i].match(/^(##)\s+(.+)$/);
|
|
110
141
|
if (m) {
|
|
111
|
-
|
|
112
|
-
indices.push({ title: m[2].trim(), index: i, level });
|
|
142
|
+
indices.push({ title: m[2].trim(), index: i });
|
|
113
143
|
}
|
|
114
144
|
}
|
|
115
145
|
for (let i = 0; i < indices.length; i++) {
|
|
116
146
|
const current = indices[i];
|
|
117
147
|
const next = indices[i + 1];
|
|
118
|
-
const
|
|
119
|
-
result[current.title] =
|
|
148
|
+
const end = next ? next.index : lines.length;
|
|
149
|
+
result[current.title] = {
|
|
150
|
+
lines: lines.slice(current.index + 1, end),
|
|
151
|
+
fenceMask: fenceMask.slice(current.index + 1, end),
|
|
152
|
+
bodyStartLine: current.index + 2,
|
|
153
|
+
};
|
|
120
154
|
}
|
|
121
155
|
return result;
|
|
122
156
|
}
|
|
157
|
+
const EMPTY_SECTION_BODY = { lines: [], fenceMask: [], bodyStartLine: 0 };
|
|
123
158
|
function getSectionCaseInsensitive(sections, desired) {
|
|
124
159
|
const target = desired.toLowerCase();
|
|
125
160
|
for (const [title, body] of Object.entries(sections)) {
|
|
126
|
-
if (title.toLowerCase() === target)
|
|
127
|
-
return { body, found: true };
|
|
161
|
+
if (title.toLowerCase() === target) {
|
|
162
|
+
return { title, body, bodyStartLine: body.bodyStartLine, found: true };
|
|
163
|
+
}
|
|
128
164
|
}
|
|
129
|
-
return { body:
|
|
165
|
+
return { title: desired, body: EMPTY_SECTION_BODY, bodyStartLine: 0, found: false };
|
|
130
166
|
}
|
|
131
|
-
function parseRequirementBlocksFromSection(sectionBody) {
|
|
132
|
-
|
|
167
|
+
function parseRequirementBlocksFromSection(sectionBody, skipped) {
|
|
168
|
+
const { lines, fenceMask } = sectionBody;
|
|
169
|
+
if (lines.length === 0)
|
|
133
170
|
return [];
|
|
134
|
-
const
|
|
171
|
+
const isRequirementHeader = (i) => !fenceMask[i] && REQUIREMENT_HEADER_REGEX.test(lines[i]);
|
|
172
|
+
const isTopLevelHeader = (i) => !fenceMask[i] && /^##\s+/.test(lines[i]);
|
|
173
|
+
const recordIfSkippedHeader = (index) => {
|
|
174
|
+
if (!skipped || fenceMask[index])
|
|
175
|
+
return;
|
|
176
|
+
const h3 = lines[index].match(/^###\s+(.+?)\s*$/);
|
|
177
|
+
if (h3 && !REQUIREMENT_HEADER_REGEX.test(lines[index])) {
|
|
178
|
+
skipped.sink.push({
|
|
179
|
+
header: h3[1].trim(),
|
|
180
|
+
section: skipped.section,
|
|
181
|
+
line: skipped.bodyStartLine + index,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
135
185
|
const blocks = [];
|
|
136
186
|
let i = 0;
|
|
137
187
|
while (i < lines.length) {
|
|
138
188
|
// Seek next requirement header
|
|
139
|
-
while (i < lines.length && !
|
|
189
|
+
while (i < lines.length && !isRequirementHeader(i)) {
|
|
190
|
+
recordIfSkippedHeader(i);
|
|
140
191
|
i++;
|
|
192
|
+
}
|
|
141
193
|
if (i >= lines.length)
|
|
142
194
|
break;
|
|
143
195
|
const headerLine = lines[i];
|
|
@@ -149,7 +201,8 @@ function parseRequirementBlocksFromSection(sectionBody) {
|
|
|
149
201
|
const name = normalizeRequirementName(m[1]);
|
|
150
202
|
const buf = [headerLine];
|
|
151
203
|
i++;
|
|
152
|
-
while (i < lines.length && !
|
|
204
|
+
while (i < lines.length && !isRequirementHeader(i) && !isTopLevelHeader(i)) {
|
|
205
|
+
recordIfSkippedHeader(i);
|
|
153
206
|
buf.push(lines[i]);
|
|
154
207
|
i++;
|
|
155
208
|
}
|
|
@@ -158,11 +211,14 @@ function parseRequirementBlocksFromSection(sectionBody) {
|
|
|
158
211
|
return blocks;
|
|
159
212
|
}
|
|
160
213
|
function parseRemovedNames(sectionBody) {
|
|
161
|
-
|
|
214
|
+
const { lines, fenceMask } = sectionBody;
|
|
215
|
+
if (lines.length === 0)
|
|
162
216
|
return [];
|
|
163
217
|
const names = [];
|
|
164
|
-
|
|
165
|
-
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
if (fenceMask[i])
|
|
220
|
+
continue;
|
|
221
|
+
const line = lines[i];
|
|
166
222
|
const m = line.match(REQUIREMENT_HEADER_REGEX);
|
|
167
223
|
if (m) {
|
|
168
224
|
names.push(normalizeRequirementName(m[1]));
|
|
@@ -177,12 +233,15 @@ function parseRemovedNames(sectionBody) {
|
|
|
177
233
|
return names;
|
|
178
234
|
}
|
|
179
235
|
function parseRenamedPairs(sectionBody) {
|
|
180
|
-
|
|
236
|
+
const { lines, fenceMask } = sectionBody;
|
|
237
|
+
if (lines.length === 0)
|
|
181
238
|
return [];
|
|
182
239
|
const pairs = [];
|
|
183
|
-
const lines = normalizeLineEndings(sectionBody).split('\n');
|
|
184
240
|
let current = {};
|
|
185
|
-
for (
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
if (fenceMask[i])
|
|
243
|
+
continue;
|
|
244
|
+
const line = lines[i];
|
|
186
245
|
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
187
246
|
const toMatch = line.match(/^\s*-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
|
|
188
247
|
if (fromMatch) {
|
|
@@ -198,4 +257,64 @@ function parseRenamedPairs(sectionBody) {
|
|
|
198
257
|
}
|
|
199
258
|
return pairs;
|
|
200
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Scenario names the current requirement block has and the incoming
|
|
262
|
+
* (MODIFIED) block does not. A MODIFIED requirement replaces the whole block,
|
|
263
|
+
* so every name reported here would be dropped from the main spec.
|
|
264
|
+
*
|
|
265
|
+
* Shared by archive (which refuses to apply the block) and validate (which
|
|
266
|
+
* reports the same loss at authoring time, #1477), so the two cannot disagree
|
|
267
|
+
* about what counts as a dropped scenario.
|
|
268
|
+
*/
|
|
269
|
+
export function findMissingCurrentScenarios(current, incoming) {
|
|
270
|
+
// Multiplicity-aware: a name present N times in current and M times in
|
|
271
|
+
// incoming means max(0, N - M) instances are missing. Set membership would
|
|
272
|
+
// treat N>M as fully covered and let archive silently drop duplicates
|
|
273
|
+
// (residual #1246 / duplicate-scenario-name blind spot).
|
|
274
|
+
const remainingIncoming = new Map();
|
|
275
|
+
for (const scenario of parseScenarioBlocks(incoming.raw)) {
|
|
276
|
+
const name = scenario.name;
|
|
277
|
+
remainingIncoming.set(name, (remainingIncoming.get(name) ?? 0) + 1);
|
|
278
|
+
}
|
|
279
|
+
const missing = [];
|
|
280
|
+
for (const scenario of parseScenarioBlocks(current.raw)) {
|
|
281
|
+
const name = scenario.name;
|
|
282
|
+
const remaining = remainingIncoming.get(name) ?? 0;
|
|
283
|
+
if (remaining > 0) {
|
|
284
|
+
remainingIncoming.set(name, remaining - 1);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
missing.push(name);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return missing;
|
|
291
|
+
}
|
|
292
|
+
function parseScenarioBlocks(requirementRaw) {
|
|
293
|
+
const lines = requirementRaw.replace(/\r\n?/g, '\n').split('\n');
|
|
294
|
+
// A `#### Scenario:` inside a fenced example is not a real scenario. The
|
|
295
|
+
// validator's countScenarios already ignores fenced lines; the drift check
|
|
296
|
+
// must agree with it, or a fenced sample can false-abort an archive (or
|
|
297
|
+
// mask a genuinely dropped scenario).
|
|
298
|
+
const mask = buildCodeFenceMask(lines);
|
|
299
|
+
const scenarios = [];
|
|
300
|
+
let index = 0;
|
|
301
|
+
while (index < lines.length) {
|
|
302
|
+
const headerMatch = mask[index] ? null : lines[index].match(/^####\s*Scenario:\s*(.+)\s*$/);
|
|
303
|
+
if (!headerMatch) {
|
|
304
|
+
index++;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const start = index;
|
|
308
|
+
const name = headerMatch[1].trim();
|
|
309
|
+
index++;
|
|
310
|
+
while (index < lines.length && (mask[index] || !/^####\s*Scenario:\s*(.+)\s*$/.test(lines[index]))) {
|
|
311
|
+
index++;
|
|
312
|
+
}
|
|
313
|
+
scenarios.push({
|
|
314
|
+
name,
|
|
315
|
+
raw: lines.slice(start, index).join('\n').trimEnd(),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return scenarios;
|
|
319
|
+
}
|
|
201
320
|
//# sourceMappingURL=requirement-blocks.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
export { buildCodeFenceMask } from './code-fence.js';
|
|
12
|
+
/**
|
|
13
|
+
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
14
|
+
* as whole words so the change-delta reader and the schema-based reader accept
|
|
15
|
+
* and reject identical text.
|
|
16
|
+
*/
|
|
17
|
+
export declare function containsShallOrMust(text: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Extract the full requirement body from the lines that follow a
|
|
20
|
+
* `### Requirement:` header (the lines may include scenarios and fenced code).
|
|
21
|
+
*
|
|
22
|
+
* Captures every body line from the start up to the first header found on a
|
|
23
|
+
* non-fenced line — usually the first `#### Scenario:`, but also a stray `###`
|
|
24
|
+
* divider the delta reader absorbed into the block — skipping blank lines and
|
|
25
|
+
* any line inside a fenced code block. `**metadata**:` lines are skipped only
|
|
26
|
+
* when other body text remains: a requirement written entirely as
|
|
27
|
+
* `**Constraint**: The system MUST ...` keeps that line as its body. Captured
|
|
28
|
+
* lines are trimmed and joined with newlines so a requirement whose text wraps
|
|
29
|
+
* across lines — or whose `SHALL`/`MUST` lands on a later line — is read in
|
|
30
|
+
* full.
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractRequirementBody(bodyLines: string[]): string;
|
|
33
|
+
/**
|
|
34
|
+
* Parser/display fallback for a requirement block with no body text. This is
|
|
35
|
+
* what lets a bare `### The system SHALL ...` header remain readable on the
|
|
36
|
+
* spec path (the title is the requirement). Validator body-keyword checks for
|
|
37
|
+
* canonical `### Requirement:` blocks use `extractRequirementBody` directly so
|
|
38
|
+
* a keyword that appears only in the header still receives the #1156/#1280
|
|
39
|
+
* body-keyword hint.
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractRequirementText(headerTitle: string, bodyLines: string[]): string;
|
|
42
|
+
/**
|
|
43
|
+
* Count the real scenarios in a requirement block: `#### ` headers on non-fenced
|
|
44
|
+
* lines. A `#### Scenario:` that lives inside a fenced example is not a real
|
|
45
|
+
* scenario and is not counted.
|
|
46
|
+
*/
|
|
47
|
+
export declare function countScenarios(bodyLines: string[]): number;
|
|
48
|
+
//# sourceMappingURL=requirement-text.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
// Re-exported so existing importers keep working; the single implementation
|
|
12
|
+
// lives in code-fence.ts.
|
|
13
|
+
export { buildCodeFenceMask } from './code-fence.js';
|
|
14
|
+
import { buildCodeFenceMask } from './code-fence.js';
|
|
15
|
+
/** Lines that look like `**ID**: ...` / `**Priority**: ...` metadata. */
|
|
16
|
+
const METADATA_LINE = /^\*\*[^*]+\*\*:/;
|
|
17
|
+
/** Any markdown header line — the boundary where a requirement body ends. */
|
|
18
|
+
const HEADER_LINE = /^#{1,6}\s/;
|
|
19
|
+
/**
|
|
20
|
+
* A level-4 header. Deliberately matches ANY `####` header, not only
|
|
21
|
+
* `#### Scenario:` — the spec path treats every level-4 child of a requirement
|
|
22
|
+
* as a scenario, so the delta counter must too (parity). Don't tighten this to
|
|
23
|
+
* `Scenario:` without changing both paths together.
|
|
24
|
+
*/
|
|
25
|
+
const SCENARIO_HEADER = /^####\s+/;
|
|
26
|
+
/**
|
|
27
|
+
* The one predicate for normative-keyword detection. Matches `SHALL` or `MUST`
|
|
28
|
+
* as whole words so the change-delta reader and the schema-based reader accept
|
|
29
|
+
* and reject identical text.
|
|
30
|
+
*/
|
|
31
|
+
export function containsShallOrMust(text) {
|
|
32
|
+
return /\b(SHALL|MUST)\b/.test(text);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract the full requirement body from the lines that follow a
|
|
36
|
+
* `### Requirement:` header (the lines may include scenarios and fenced code).
|
|
37
|
+
*
|
|
38
|
+
* Captures every body line from the start up to the first header found on a
|
|
39
|
+
* non-fenced line — usually the first `#### Scenario:`, but also a stray `###`
|
|
40
|
+
* divider the delta reader absorbed into the block — skipping blank lines and
|
|
41
|
+
* any line inside a fenced code block. `**metadata**:` lines are skipped only
|
|
42
|
+
* when other body text remains: a requirement written entirely as
|
|
43
|
+
* `**Constraint**: The system MUST ...` keeps that line as its body. Captured
|
|
44
|
+
* lines are trimmed and joined with newlines so a requirement whose text wraps
|
|
45
|
+
* across lines — or whose `SHALL`/`MUST` lands on a later line — is read in
|
|
46
|
+
* full.
|
|
47
|
+
*/
|
|
48
|
+
export function extractRequirementBody(bodyLines) {
|
|
49
|
+
const mask = buildCodeFenceMask(bodyLines);
|
|
50
|
+
const captured = [];
|
|
51
|
+
const metadata = [];
|
|
52
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
53
|
+
if (mask[i])
|
|
54
|
+
continue; // inside a fenced code block
|
|
55
|
+
const line = bodyLines[i];
|
|
56
|
+
if (HEADER_LINE.test(line))
|
|
57
|
+
break; // first scenario or stray divider
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (trimmed.length === 0)
|
|
60
|
+
continue; // blank
|
|
61
|
+
if (METADATA_LINE.test(trimmed)) {
|
|
62
|
+
metadata.push(trimmed); // **ID**: / **Priority**: ...
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
captured.push(trimmed);
|
|
66
|
+
}
|
|
67
|
+
if (captured.length > 0)
|
|
68
|
+
return captured.join('\n');
|
|
69
|
+
return metadata.join('\n'); // metadata-only body: the metadata IS the body
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parser/display fallback for a requirement block with no body text. This is
|
|
73
|
+
* what lets a bare `### The system SHALL ...` header remain readable on the
|
|
74
|
+
* spec path (the title is the requirement). Validator body-keyword checks for
|
|
75
|
+
* canonical `### Requirement:` blocks use `extractRequirementBody` directly so
|
|
76
|
+
* a keyword that appears only in the header still receives the #1156/#1280
|
|
77
|
+
* body-keyword hint.
|
|
78
|
+
*/
|
|
79
|
+
export function extractRequirementText(headerTitle, bodyLines) {
|
|
80
|
+
return extractRequirementBody(bodyLines) || headerTitle.trim();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Count the real scenarios in a requirement block: `#### ` headers on non-fenced
|
|
84
|
+
* lines. A `#### Scenario:` that lives inside a fenced example is not a real
|
|
85
|
+
* scenario and is not counted.
|
|
86
|
+
*/
|
|
87
|
+
export function countScenarios(bodyLines) {
|
|
88
|
+
const mask = buildCodeFenceMask(bodyLines);
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
91
|
+
if (mask[i])
|
|
92
|
+
continue;
|
|
93
|
+
if (SCENARIO_HEADER.test(bodyLines[i]))
|
|
94
|
+
count++;
|
|
95
|
+
}
|
|
96
|
+
return count;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=requirement-text.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildCodeFenceMask } from './code-fence.js';
|
|
1
2
|
const REQUIREMENTS_SECTION_HEADER = /^##\s+Requirements\s*$/i;
|
|
2
3
|
const TOP_LEVEL_SECTION_HEADER = /^##\s+/;
|
|
3
4
|
const DELTA_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
@@ -7,6 +8,7 @@ export function findMainSpecStructureIssues(content) {
|
|
|
7
8
|
const stripped = stripFencedCodeBlocksPreservingLines(normalized);
|
|
8
9
|
const lines = stripped.split('\n');
|
|
9
10
|
const issues = [];
|
|
11
|
+
const requirementLines = new Map();
|
|
10
12
|
const requirementsHeaderIndex = lines.findIndex(line => REQUIREMENTS_SECTION_HEADER.test(line));
|
|
11
13
|
let requirementsEndIndex = lines.length;
|
|
12
14
|
if (requirementsHeaderIndex !== -1) {
|
|
@@ -29,7 +31,7 @@ export function findMainSpecStructureIssues(content) {
|
|
|
29
31
|
line: i + 1,
|
|
30
32
|
header: trimmed,
|
|
31
33
|
message: `Main spec contains delta header "${trimmed}". ` +
|
|
32
|
-
'Delta headers are only valid inside openspec/changes/<name>/specs/<capability>/spec.md ' +
|
|
34
|
+
'Delta headers are only valid inside openspec/changes/<name>/specs/<capability-path>/spec.md ' +
|
|
33
35
|
'and truncate the parsed ## Requirements section.',
|
|
34
36
|
});
|
|
35
37
|
continue;
|
|
@@ -49,40 +51,28 @@ export function findMainSpecStructureIssues(content) {
|
|
|
49
51
|
message: `Requirement header "${trimmed}" appears outside the main ## Requirements section. ` +
|
|
50
52
|
'Main specs only parse requirements inside that section, so this requirement is currently invisible to validate, list, and archive.',
|
|
51
53
|
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const requirementName = requirementMatch[1].trim();
|
|
57
|
+
const previousLine = requirementLines.get(requirementName);
|
|
58
|
+
if (previousLine !== undefined) {
|
|
59
|
+
issues.push({
|
|
60
|
+
kind: 'duplicate-requirement',
|
|
61
|
+
line: i + 1,
|
|
62
|
+
header: trimmed,
|
|
63
|
+
message: `Requirement header "${trimmed}" duplicates the requirement declared on line ${previousLine}. ` +
|
|
64
|
+
'Requirement names must be unique so spec updates cannot discard one block while updating another.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
requirementLines.set(requirementName, i + 1);
|
|
52
69
|
}
|
|
53
70
|
}
|
|
54
71
|
return issues;
|
|
55
72
|
}
|
|
56
73
|
export function stripFencedCodeBlocksPreservingLines(content) {
|
|
57
74
|
const lines = content.split('\n');
|
|
58
|
-
const
|
|
59
|
-
|
|
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);
|
|
75
|
+
const mask = buildCodeFenceMask(lines);
|
|
76
|
+
return lines.map((line, i) => (mask[i] ? '' : line)).join('\n');
|
|
87
77
|
}
|
|
88
78
|
//# sourceMappingURL=spec-structure.js.map
|
|
@@ -5,14 +5,6 @@ type WorkflowId = (typeof ALL_WORKFLOWS)[number];
|
|
|
5
5
|
* Maps workflow IDs to their skill directory names.
|
|
6
6
|
*/
|
|
7
7
|
export declare const WORKFLOW_TO_SKILL_DIR: Record<WorkflowId, string>;
|
|
8
|
-
/**
|
|
9
|
-
* Checks whether a tool has at least one generated OpenSpec command file.
|
|
10
|
-
*/
|
|
11
|
-
export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Returns tools with at least one generated command file on disk.
|
|
14
|
-
*/
|
|
15
|
-
export declare function getCommandConfiguredTools(projectPath: string): string[];
|
|
16
8
|
/**
|
|
17
9
|
* Returns tools that are configured via either skills or commands.
|
|
18
10
|
*/
|