@gordon.gan/specflow 1.0.0
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 +31 -0
- package/README.md +442 -0
- package/bin/specflow.js +2 -0
- package/dist/cli/commands/change-archive.d.ts +26 -0
- package/dist/cli/commands/change-archive.js +50 -0
- package/dist/cli/commands/change-new.d.ts +18 -0
- package/dist/cli/commands/change-new.js +53 -0
- package/dist/cli/commands/change-phase.d.ts +33 -0
- package/dist/cli/commands/change-phase.js +66 -0
- package/dist/cli/commands/change-status.d.ts +35 -0
- package/dist/cli/commands/change-status.js +95 -0
- package/dist/cli/commands/doctor.d.ts +9 -0
- package/dist/cli/commands/doctor.js +48 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +122 -0
- package/dist/cli/commands/instructions.d.ts +21 -0
- package/dist/cli/commands/instructions.js +75 -0
- package/dist/cli/commands/parity-report.d.ts +10 -0
- package/dist/cli/commands/parity-report.js +67 -0
- package/dist/cli/commands/sync.d.ts +4 -0
- package/dist/cli/commands/sync.js +40 -0
- package/dist/cli/commands/validate.d.ts +21 -0
- package/dist/cli/commands/validate.js +46 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +52 -0
- package/dist/core/archive.d.ts +39 -0
- package/dist/core/archive.js +154 -0
- package/dist/core/artifact-graph/graph.d.ts +60 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +10 -0
- package/dist/core/artifact-graph/index.js +7 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +45 -0
- package/dist/core/artifact-graph/instruction-loader.js +37 -0
- package/dist/core/artifact-graph/outputs.d.ts +35 -0
- package/dist/core/artifact-graph/outputs.js +48 -0
- package/dist/core/artifact-graph/resolver.d.ts +54 -0
- package/dist/core/artifact-graph/resolver.js +139 -0
- package/dist/core/artifact-graph/schema.d.ts +23 -0
- package/dist/core/artifact-graph/schema.js +115 -0
- package/dist/core/artifact-graph/state.d.ts +9 -0
- package/dist/core/artifact-graph/state.js +93 -0
- package/dist/core/artifact-graph/types.d.ts +123 -0
- package/dist/core/artifact-graph/types.js +29 -0
- package/dist/core/parsers/change-parser.d.ts +20 -0
- package/dist/core/parsers/change-parser.js +22 -0
- package/dist/core/parsers/index.d.ts +7 -0
- package/dist/core/parsers/index.js +4 -0
- package/dist/core/parsers/markdown-parser.d.ts +35 -0
- package/dist/core/parsers/markdown-parser.js +193 -0
- package/dist/core/parsers/requirement-blocks.d.ts +21 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/parsers/spec-structure.d.ts +22 -0
- package/dist/core/parsers/spec-structure.js +105 -0
- package/dist/core/specs-apply.d.ts +22 -0
- package/dist/core/specs-apply.js +165 -0
- package/dist/core/templates/index.d.ts +31 -0
- package/dist/core/templates/index.js +80 -0
- package/dist/core/templates/types.d.ts +4 -0
- package/dist/core/templates/types.js +1 -0
- package/dist/core/validation/constants.d.ts +14 -0
- package/dist/core/validation/constants.js +14 -0
- package/dist/core/validation/types.d.ts +13 -0
- package/dist/core/validation/types.js +6 -0
- package/dist/core/validation/validator.d.ts +24 -0
- package/dist/core/validation/validator.js +250 -0
- package/dist/integrations/claude/adapter.d.ts +2 -0
- package/dist/integrations/claude/adapter.js +106 -0
- package/dist/integrations/codex/adapter.d.ts +2 -0
- package/dist/integrations/codex/adapter.js +141 -0
- package/dist/integrations/cursor/adapter.d.ts +2 -0
- package/dist/integrations/cursor/adapter.js +126 -0
- package/dist/integrations/shared/adapter-registry.d.ts +4 -0
- package/dist/integrations/shared/adapter-registry.js +27 -0
- package/dist/integrations/shared/asset-copy.d.ts +2 -0
- package/dist/integrations/shared/asset-copy.js +44 -0
- package/dist/integrations/shared/asset-hash.d.ts +4 -0
- package/dist/integrations/shared/asset-hash.js +54 -0
- package/dist/integrations/shared/capability-evidence.d.ts +1 -0
- package/dist/integrations/shared/capability-evidence.js +42 -0
- package/dist/integrations/shared/command-catalog.d.ts +6 -0
- package/dist/integrations/shared/command-catalog.js +13 -0
- package/dist/integrations/shared/managed-assets.d.ts +2 -0
- package/dist/integrations/shared/managed-assets.js +21 -0
- package/dist/integrations/shared/marker-write.d.ts +3 -0
- package/dist/integrations/shared/marker-write.js +48 -0
- package/dist/integrations/shared/migration-state.d.ts +2 -0
- package/dist/integrations/shared/migration-state.js +67 -0
- package/dist/integrations/shared/parity-comparator.d.ts +6 -0
- package/dist/integrations/shared/parity-comparator.js +97 -0
- package/dist/integrations/shared/parity-manifest.d.ts +13 -0
- package/dist/integrations/shared/parity-manifest.js +58 -0
- package/dist/integrations/shared/runtime-assets.d.ts +2 -0
- package/dist/integrations/shared/runtime-assets.js +21 -0
- package/dist/integrations/shared/skill-renderer.d.ts +5 -0
- package/dist/integrations/shared/skill-renderer.js +77 -0
- package/dist/integrations/shared/types.d.ts +35 -0
- package/dist/integrations/shared/types.js +1 -0
- package/dist/utils/change-metadata.d.ts +38 -0
- package/dist/utils/change-metadata.js +82 -0
- package/dist/utils/change-utils.d.ts +53 -0
- package/dist/utils/change-utils.js +97 -0
- package/dist/utils/file-system.d.ts +35 -0
- package/dist/utils/file-system.js +67 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/project-root.d.ts +22 -0
- package/dist/utils/project-root.js +44 -0
- package/dist/utils/task-progress.d.ts +39 -0
- package/dist/utils/task-progress.js +62 -0
- package/package.json +72 -0
- package/prompts/build/ecc-go-reviewer.md +80 -0
- package/prompts/build/ecc-java-reviewer.md +185 -0
- package/prompts/build/ecc-kotlin-reviewer.md +163 -0
- package/prompts/build/ecc-python-reviewer.md +102 -0
- package/prompts/build/ecc-rust-reviewer.md +98 -0
- package/prompts/build/ecc-typescript-reviewer.md +116 -0
- package/prompts/build/language-router.md +22 -0
- package/prompts/build/phase-a-plan.md +288 -0
- package/prompts/build/phase-b-execute.md +276 -0
- package/prompts/build/phase-b-review.md +119 -0
- package/prompts/build/phase-b-worktree.md +215 -0
- package/prompts/build/plan-document-reviewer.md +51 -0
- package/prompts/build/tdd.md +184 -0
- package/prompts/done/branch-finish.md +204 -0
- package/prompts/fix/debug.md +249 -0
- package/prompts/plan/design-draft.md +123 -0
- package/prompts/plan/proposal.md +85 -0
- package/prompts/plan/specs.md +114 -0
- package/prompts/plan/tasks-draft.md +123 -0
- package/prompts/reference/openspec/apply-instruction.md +2 -0
- package/prompts/reference/openspec/design-instruction.md +20 -0
- package/prompts/reference/openspec/proposal-instruction.md +18 -0
- package/prompts/reference/openspec/specs-instruction.md +47 -0
- package/prompts/reference/openspec/tasks-instruction.md +26 -0
- package/prompts/reference/specflow/example-design.md +284 -0
- package/prompts/reference/superpowers/anthropic-best-practices.md +1150 -0
- package/prompts/reference/superpowers/codex-tools.md +29 -0
- package/prompts/reference/superpowers/condition-based-waiting-example.ts +158 -0
- package/prompts/reference/superpowers/condition-based-waiting.md +115 -0
- package/prompts/reference/superpowers/defense-in-depth.md +122 -0
- package/prompts/reference/superpowers/find-polluter.sh +63 -0
- package/prompts/reference/superpowers/graphviz-conventions.dot +172 -0
- package/prompts/reference/superpowers/persuasion-principles.md +187 -0
- package/prompts/reference/superpowers/root-cause-tracing.md +169 -0
- package/prompts/reference/superpowers/testing-anti-patterns.md +299 -0
- package/prompts/reference/superpowers/testing-skills-with-subagents.md +384 -0
- package/prompts/reference/superpowers/using-superpowers.md +117 -0
- package/prompts/reference/superpowers/writing-skills.md +655 -0
- package/prompts/refine/brainstorm.md +241 -0
- package/prompts/refine/design-output.md +126 -0
- package/prompts/refine/spec-document-reviewer.md +51 -0
- package/prompts/refine/update-artifacts.md +185 -0
- package/prompts/review/code-review.md +223 -0
- package/prompts/shared/code-reviewer-prompt.md +98 -0
- package/prompts/shared/dispatching-parallel-agents.md +143 -0
- package/prompts/shared/executing-plans.md +67 -0
- package/prompts/shared/implementer-prompt.md +115 -0
- package/prompts/shared/receiving-code-review.md +174 -0
- package/prompts/shared/spec-reviewer-prompt.md +63 -0
- package/prompts/test/tdd.md +236 -0
- package/prompts/test/verification.md +129 -0
- package/prompts/verify/ecc-security-reviewer.md +112 -0
- package/prompts/verify/verification.md +196 -0
- package/schemas/specflow/schema.yaml +48 -0
- package/skills/specflow-build/SKILL.md +129 -0
- package/skills/specflow-done/SKILL.md +68 -0
- package/skills/specflow-fix/SKILL.md +74 -0
- package/skills/specflow-plan/SKILL.md +82 -0
- package/skills/specflow-refine/SKILL.md +128 -0
- package/skills/specflow-review/SKILL.md +40 -0
- package/skills/specflow-scan/SKILL.md +48 -0
- package/skills/specflow-snap/SKILL.md +46 -0
- package/skills/specflow-test/SKILL.md +48 -0
- package/skills/specflow-verify/SKILL.md +77 -0
- package/templates/design.md +19 -0
- package/templates/proposal.md +23 -0
- package/templates/spec.md +42 -0
- package/templates/tasks.md +9 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown delta-spec parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses a delta-spec markdown file into structured data with
|
|
5
|
+
* ADDED, MODIFIED, REMOVED, and RENAMED sections.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Internal helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const SECTION_HEADER = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\s*$/i;
|
|
11
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/;
|
|
12
|
+
const SCENARIO_HEADER = /^####\s+Scenario:\s*(.+)\s*$/;
|
|
13
|
+
const WHEN_LINE = /^-\s+\*\*WHEN\*\*\s+(.+)$/;
|
|
14
|
+
const THEN_LINE = /^-\s+\*\*THEN\*\*\s+(.+)$/;
|
|
15
|
+
const REASON_LINE = /^\*\*Reason\*\*:\s*(.+)$/;
|
|
16
|
+
const MIGRATION_LINE = /^\*\*Migration\*\*:\s*(.+)$/;
|
|
17
|
+
const FROM_LINE = /^FROM:\s*(.+)$/;
|
|
18
|
+
const TO_LINE = /^TO:\s*(.+)$/;
|
|
19
|
+
function splitSections(content) {
|
|
20
|
+
const lines = content.replace(/\r\n?/g, '\n').split('\n');
|
|
21
|
+
const sections = [];
|
|
22
|
+
let current = null;
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
const match = SECTION_HEADER.exec(line);
|
|
25
|
+
if (match) {
|
|
26
|
+
if (current) {
|
|
27
|
+
sections.push({ kind: current.kind, lines: current.lines });
|
|
28
|
+
}
|
|
29
|
+
current = {
|
|
30
|
+
kind: match[1].toUpperCase(),
|
|
31
|
+
lines: [],
|
|
32
|
+
};
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (current) {
|
|
36
|
+
current.lines.push(line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (current) {
|
|
40
|
+
sections.push({ kind: current.kind, lines: current.lines });
|
|
41
|
+
}
|
|
42
|
+
return sections;
|
|
43
|
+
}
|
|
44
|
+
function parseScenarios(lines) {
|
|
45
|
+
const scenarios = [];
|
|
46
|
+
let current = null;
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const scenarioMatch = SCENARIO_HEADER.exec(line);
|
|
49
|
+
if (scenarioMatch) {
|
|
50
|
+
if (current) {
|
|
51
|
+
scenarios.push({ ...current });
|
|
52
|
+
}
|
|
53
|
+
current = { name: scenarioMatch[1].trim(), when: [], then: [] };
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!current)
|
|
57
|
+
continue;
|
|
58
|
+
const whenMatch = WHEN_LINE.exec(line);
|
|
59
|
+
if (whenMatch) {
|
|
60
|
+
current.when.push(whenMatch[1].trim());
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const thenMatch = THEN_LINE.exec(line);
|
|
64
|
+
if (thenMatch) {
|
|
65
|
+
current.then.push(thenMatch[1].trim());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (current) {
|
|
69
|
+
scenarios.push({ ...current });
|
|
70
|
+
}
|
|
71
|
+
return scenarios;
|
|
72
|
+
}
|
|
73
|
+
function parseRequirementBlocks(lines) {
|
|
74
|
+
const blocks = [];
|
|
75
|
+
let currentName = null;
|
|
76
|
+
let blockLines = [];
|
|
77
|
+
const flush = () => {
|
|
78
|
+
if (currentName !== null) {
|
|
79
|
+
const descriptionLines = [];
|
|
80
|
+
const scenarioLines = [];
|
|
81
|
+
let inScenario = false;
|
|
82
|
+
for (const l of blockLines) {
|
|
83
|
+
if (SCENARIO_HEADER.test(l)) {
|
|
84
|
+
inScenario = true;
|
|
85
|
+
}
|
|
86
|
+
if (inScenario) {
|
|
87
|
+
scenarioLines.push(l);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
descriptionLines.push(l);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
blocks.push({
|
|
94
|
+
name: currentName,
|
|
95
|
+
description: descriptionLines
|
|
96
|
+
.join('\n')
|
|
97
|
+
.trim(),
|
|
98
|
+
scenarios: parseScenarios(scenarioLines),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const reqMatch = REQUIREMENT_HEADER.exec(line);
|
|
104
|
+
if (reqMatch) {
|
|
105
|
+
flush();
|
|
106
|
+
currentName = reqMatch[1].trim();
|
|
107
|
+
blockLines = [];
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
blockLines.push(line);
|
|
111
|
+
}
|
|
112
|
+
flush();
|
|
113
|
+
return blocks;
|
|
114
|
+
}
|
|
115
|
+
function parseRemovedRequirements(lines) {
|
|
116
|
+
const removed = [];
|
|
117
|
+
let currentName = null;
|
|
118
|
+
let reason = '';
|
|
119
|
+
let migration = '';
|
|
120
|
+
const flush = () => {
|
|
121
|
+
if (currentName !== null) {
|
|
122
|
+
removed.push({ name: currentName, reason, migration });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const reqMatch = REQUIREMENT_HEADER.exec(line);
|
|
127
|
+
if (reqMatch) {
|
|
128
|
+
flush();
|
|
129
|
+
currentName = reqMatch[1].trim();
|
|
130
|
+
reason = '';
|
|
131
|
+
migration = '';
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const reasonMatch = REASON_LINE.exec(line);
|
|
135
|
+
if (reasonMatch) {
|
|
136
|
+
reason = reasonMatch[1].trim();
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const migrationMatch = MIGRATION_LINE.exec(line);
|
|
140
|
+
if (migrationMatch) {
|
|
141
|
+
migration = migrationMatch[1].trim();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
flush();
|
|
145
|
+
return removed;
|
|
146
|
+
}
|
|
147
|
+
function parseRenamedRequirements(lines) {
|
|
148
|
+
const renamed = [];
|
|
149
|
+
let from = null;
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
const fromMatch = FROM_LINE.exec(line);
|
|
152
|
+
if (fromMatch) {
|
|
153
|
+
from = fromMatch[1].trim();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const toMatch = TO_LINE.exec(line);
|
|
157
|
+
if (toMatch && from !== null) {
|
|
158
|
+
renamed.push({ from, to: toMatch[1].trim() });
|
|
159
|
+
from = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return renamed;
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Public API
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
/**
|
|
168
|
+
* Parse a delta-spec markdown file into structured data.
|
|
169
|
+
*/
|
|
170
|
+
export function parseDeltaSpec(content) {
|
|
171
|
+
const sections = splitSections(content);
|
|
172
|
+
let added = [];
|
|
173
|
+
let modified = [];
|
|
174
|
+
let removed = [];
|
|
175
|
+
let renamed = [];
|
|
176
|
+
for (const section of sections) {
|
|
177
|
+
switch (section.kind) {
|
|
178
|
+
case 'ADDED':
|
|
179
|
+
added = parseRequirementBlocks(section.lines);
|
|
180
|
+
break;
|
|
181
|
+
case 'MODIFIED':
|
|
182
|
+
modified = parseRequirementBlocks(section.lines);
|
|
183
|
+
break;
|
|
184
|
+
case 'REMOVED':
|
|
185
|
+
removed = parseRemovedRequirements(section.lines);
|
|
186
|
+
break;
|
|
187
|
+
case 'RENAMED':
|
|
188
|
+
renamed = parseRenamedRequirements(section.lines);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { added, modified, removed, renamed };
|
|
193
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirement-blocks parser.
|
|
3
|
+
*
|
|
4
|
+
* Utilities for extracting individual requirement names and blocks
|
|
5
|
+
* from markdown content.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extract the requirement name from a `### Requirement: <name>` header line.
|
|
9
|
+
* Returns the trimmed name, or an empty string if the line is not a valid header.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractRequirementName(header: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Extract the full block for a named requirement from markdown content.
|
|
14
|
+
*
|
|
15
|
+
* The block starts at `### Requirement: <name>` and extends through all
|
|
16
|
+
* content (including scenarios) until the next `### Requirement:` header,
|
|
17
|
+
* a `## ` section header, or end-of-content.
|
|
18
|
+
*
|
|
19
|
+
* Returns the block as a trimmed string, or an empty string if not found.
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractRequirementBlock(content: string, name: string): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Requirement-blocks parser.
|
|
3
|
+
*
|
|
4
|
+
* Utilities for extracting individual requirement names and blocks
|
|
5
|
+
* from markdown content.
|
|
6
|
+
*/
|
|
7
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/;
|
|
8
|
+
const SECTION_BOUNDARY = /^##\s+/;
|
|
9
|
+
const NEXT_REQUIREMENT = /^###\s+Requirement:\s*/;
|
|
10
|
+
/**
|
|
11
|
+
* Extract the requirement name from a `### Requirement: <name>` header line.
|
|
12
|
+
* Returns the trimmed name, or an empty string if the line is not a valid header.
|
|
13
|
+
*/
|
|
14
|
+
export function extractRequirementName(header) {
|
|
15
|
+
const match = REQUIREMENT_HEADER.exec(header.trim());
|
|
16
|
+
return match ? match[1].trim() : '';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Extract the full block for a named requirement from markdown content.
|
|
20
|
+
*
|
|
21
|
+
* The block starts at `### Requirement: <name>` and extends through all
|
|
22
|
+
* content (including scenarios) until the next `### Requirement:` header,
|
|
23
|
+
* a `## ` section header, or end-of-content.
|
|
24
|
+
*
|
|
25
|
+
* Returns the block as a trimmed string, or an empty string if not found.
|
|
26
|
+
*/
|
|
27
|
+
export function extractRequirementBlock(content, name) {
|
|
28
|
+
const normalized = content.replace(/\r\n?/g, '\n');
|
|
29
|
+
const lines = normalized.split('\n');
|
|
30
|
+
// Find the header line for this requirement
|
|
31
|
+
const targetPattern = new RegExp(`^###\\s+Requirement:\\s*${escapeRegExp(name)}\\s*$`);
|
|
32
|
+
let startIndex = -1;
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
if (targetPattern.test(lines[i])) {
|
|
35
|
+
startIndex = i;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (startIndex === -1) {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
// Collect lines until a boundary
|
|
43
|
+
let endIndex = lines.length;
|
|
44
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
45
|
+
if (NEXT_REQUIREMENT.test(lines[i]) || SECTION_BOUNDARY.test(lines[i])) {
|
|
46
|
+
endIndex = i;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return lines.slice(startIndex, endIndex).join('\n').trimEnd();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Escape special regex characters in a string for use in a RegExp constructor.
|
|
54
|
+
*/
|
|
55
|
+
function escapeRegExp(str) {
|
|
56
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec structure parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses a main spec markdown file into a structured ParsedSpec
|
|
5
|
+
* containing typed requirements and their scenarios.
|
|
6
|
+
*/
|
|
7
|
+
import type { Scenario } from './markdown-parser.js';
|
|
8
|
+
export interface Requirement {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
readonly scenarios: readonly Scenario[];
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedSpec {
|
|
14
|
+
readonly requirements: readonly Requirement[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse a main spec markdown file into a ParsedSpec.
|
|
18
|
+
*
|
|
19
|
+
* Extracts all requirements from the `## Requirements` section,
|
|
20
|
+
* including their descriptions and scenarios.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseMainSpec(content: string): ParsedSpec;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec structure parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses a main spec markdown file into a structured ParsedSpec
|
|
5
|
+
* containing typed requirements and their scenarios.
|
|
6
|
+
*/
|
|
7
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/;
|
|
8
|
+
const SCENARIO_HEADER = /^####\s+Scenario:\s*(.+)\s*$/;
|
|
9
|
+
const SECTION_BOUNDARY = /^##\s+/;
|
|
10
|
+
const WHEN_LINE = /^-\s+\*\*WHEN\*\*\s+(.+)$/;
|
|
11
|
+
const THEN_LINE = /^-\s+\*\*THEN\*\*\s+(.+)$/;
|
|
12
|
+
/**
|
|
13
|
+
* Parse a main spec markdown file into a ParsedSpec.
|
|
14
|
+
*
|
|
15
|
+
* Extracts all requirements from the `## Requirements` section,
|
|
16
|
+
* including their descriptions and scenarios.
|
|
17
|
+
*/
|
|
18
|
+
export function parseMainSpec(content) {
|
|
19
|
+
const normalized = content.replace(/\r\n?/g, '\n');
|
|
20
|
+
const lines = normalized.split('\n');
|
|
21
|
+
// Find the ## Requirements section
|
|
22
|
+
const reqSectionIndex = lines.findIndex((line) => /^##\s+Requirements\s*$/i.test(line));
|
|
23
|
+
if (reqSectionIndex === -1) {
|
|
24
|
+
return { requirements: [] };
|
|
25
|
+
}
|
|
26
|
+
// Find the end of the Requirements section
|
|
27
|
+
let endIndex = lines.length;
|
|
28
|
+
for (let i = reqSectionIndex + 1; i < lines.length; i++) {
|
|
29
|
+
if (SECTION_BOUNDARY.test(lines[i]) && !/^##\s+Requirements/i.test(lines[i])) {
|
|
30
|
+
endIndex = i;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const sectionLines = lines.slice(reqSectionIndex + 1, endIndex);
|
|
35
|
+
return { requirements: parseRequirements(sectionLines) };
|
|
36
|
+
}
|
|
37
|
+
function parseRequirements(lines) {
|
|
38
|
+
const requirements = [];
|
|
39
|
+
let currentName = null;
|
|
40
|
+
let descriptionLines = [];
|
|
41
|
+
let scenarioLines = [];
|
|
42
|
+
let inScenario = false;
|
|
43
|
+
const flush = () => {
|
|
44
|
+
if (currentName !== null) {
|
|
45
|
+
requirements.push({
|
|
46
|
+
name: currentName,
|
|
47
|
+
description: descriptionLines.join('\n').trim(),
|
|
48
|
+
scenarios: parseScenarios(scenarioLines),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const reqMatch = REQUIREMENT_HEADER.exec(line);
|
|
54
|
+
if (reqMatch) {
|
|
55
|
+
flush();
|
|
56
|
+
currentName = reqMatch[1].trim();
|
|
57
|
+
descriptionLines = [];
|
|
58
|
+
scenarioLines = [];
|
|
59
|
+
inScenario = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (currentName === null)
|
|
63
|
+
continue;
|
|
64
|
+
if (SCENARIO_HEADER.test(line)) {
|
|
65
|
+
inScenario = true;
|
|
66
|
+
}
|
|
67
|
+
if (inScenario) {
|
|
68
|
+
scenarioLines.push(line);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
descriptionLines.push(line);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
flush();
|
|
75
|
+
return requirements;
|
|
76
|
+
}
|
|
77
|
+
function parseScenarios(lines) {
|
|
78
|
+
const scenarios = [];
|
|
79
|
+
let current = null;
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const scenarioMatch = SCENARIO_HEADER.exec(line);
|
|
82
|
+
if (scenarioMatch) {
|
|
83
|
+
if (current) {
|
|
84
|
+
scenarios.push({ ...current });
|
|
85
|
+
}
|
|
86
|
+
current = { name: scenarioMatch[1].trim(), when: [], then: [] };
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!current)
|
|
90
|
+
continue;
|
|
91
|
+
const whenMatch = WHEN_LINE.exec(line);
|
|
92
|
+
if (whenMatch) {
|
|
93
|
+
current.when.push(whenMatch[1].trim());
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const thenMatch = THEN_LINE.exec(line);
|
|
97
|
+
if (thenMatch) {
|
|
98
|
+
current.then.push(thenMatch[1].trim());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (current) {
|
|
102
|
+
scenarios.push({ ...current });
|
|
103
|
+
}
|
|
104
|
+
return scenarios;
|
|
105
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specs apply.
|
|
3
|
+
*
|
|
4
|
+
* Applies a delta spec to a main spec, producing an updated main spec.
|
|
5
|
+
* Handles ADDED, MODIFIED, REMOVED, and RENAMED operations on
|
|
6
|
+
* requirement blocks identified by `### Requirement: <name>` headers.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Apply a delta spec to a main spec.
|
|
10
|
+
*
|
|
11
|
+
* Operations are applied in this order:
|
|
12
|
+
* 1. RENAMED - rename requirement headers
|
|
13
|
+
* 2. REMOVED - delete requirement blocks
|
|
14
|
+
* 3. MODIFIED - replace requirement blocks
|
|
15
|
+
* 4. ADDED - append new requirement blocks
|
|
16
|
+
*
|
|
17
|
+
* @param mainSpecContent - The raw markdown of the main spec
|
|
18
|
+
* @param deltaContent - The raw markdown of the delta spec
|
|
19
|
+
* @returns The updated main spec markdown
|
|
20
|
+
* @throws When a MODIFIED target does not exist in the main spec
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyDeltaSpec(mainSpecContent: string, deltaContent: string): string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specs apply.
|
|
3
|
+
*
|
|
4
|
+
* Applies a delta spec to a main spec, producing an updated main spec.
|
|
5
|
+
* Handles ADDED, MODIFIED, REMOVED, and RENAMED operations on
|
|
6
|
+
* requirement blocks identified by `### Requirement: <name>` headers.
|
|
7
|
+
*/
|
|
8
|
+
import { parseDeltaSpec } from './parsers/markdown-parser.js';
|
|
9
|
+
const REQUIREMENT_HEADER = /^###\s+Requirement:\s*(.+)\s*$/;
|
|
10
|
+
/**
|
|
11
|
+
* Find all requirement block spans in lines array.
|
|
12
|
+
* Each span covers from the `### Requirement:` line to just before
|
|
13
|
+
* the next requirement header or end-of-content.
|
|
14
|
+
*/
|
|
15
|
+
function findBlockSpans(lines) {
|
|
16
|
+
const spans = [];
|
|
17
|
+
let currentName = null;
|
|
18
|
+
let currentStart = -1;
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const match = REQUIREMENT_HEADER.exec(lines[i]);
|
|
21
|
+
if (match) {
|
|
22
|
+
if (currentName !== null) {
|
|
23
|
+
spans.push({ name: currentName, startIndex: currentStart, endIndex: i });
|
|
24
|
+
}
|
|
25
|
+
currentName = match[1].trim();
|
|
26
|
+
currentStart = i;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (currentName !== null) {
|
|
30
|
+
spans.push({ name: currentName, startIndex: currentStart, endIndex: lines.length });
|
|
31
|
+
}
|
|
32
|
+
return spans;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Rebuild a requirement block's markdown from a parsed RequirementBlock.
|
|
36
|
+
*/
|
|
37
|
+
function blockToMarkdown(block) {
|
|
38
|
+
const parts = [`### Requirement: ${block.name}`];
|
|
39
|
+
if (block.description.trim()) {
|
|
40
|
+
parts.push(block.description);
|
|
41
|
+
}
|
|
42
|
+
for (const scenario of block.scenarios) {
|
|
43
|
+
parts.push('');
|
|
44
|
+
parts.push(`#### Scenario: ${scenario.name}`);
|
|
45
|
+
for (const w of scenario.when) {
|
|
46
|
+
parts.push(`- **WHEN** ${w}`);
|
|
47
|
+
}
|
|
48
|
+
for (const t of scenario.then) {
|
|
49
|
+
parts.push(`- **THEN** ${t}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return parts.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Remove a requirement block from the lines array by name.
|
|
56
|
+
* Returns a new array without the block, trimming excess blank lines.
|
|
57
|
+
*/
|
|
58
|
+
function removeBlock(lines, name) {
|
|
59
|
+
const spans = findBlockSpans(lines);
|
|
60
|
+
const target = spans.find((s) => s.name === name);
|
|
61
|
+
if (!target) {
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
const before = lines.slice(0, target.startIndex);
|
|
65
|
+
const after = lines.slice(target.endIndex);
|
|
66
|
+
// Trim trailing blank lines from before and leading blank lines from after
|
|
67
|
+
const trimmedBefore = trimTrailingBlanks(before);
|
|
68
|
+
const trimmedAfter = trimLeadingBlanks(after);
|
|
69
|
+
if (trimmedBefore.length === 0) {
|
|
70
|
+
return trimmedAfter;
|
|
71
|
+
}
|
|
72
|
+
if (trimmedAfter.length === 0) {
|
|
73
|
+
return trimmedBefore;
|
|
74
|
+
}
|
|
75
|
+
return [...trimmedBefore, '', ...trimmedAfter];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Replace a requirement block's content in the lines array.
|
|
79
|
+
* Returns a new array with the block replaced.
|
|
80
|
+
*/
|
|
81
|
+
function replaceBlock(lines, name, newBlock) {
|
|
82
|
+
const spans = findBlockSpans(lines);
|
|
83
|
+
const target = spans.find((s) => s.name === name);
|
|
84
|
+
if (!target) {
|
|
85
|
+
throw new Error(`Requirement "${name}" not found in main spec`);
|
|
86
|
+
}
|
|
87
|
+
const before = lines.slice(0, target.startIndex);
|
|
88
|
+
const after = lines.slice(target.endIndex);
|
|
89
|
+
const replacement = blockToMarkdown(newBlock).split('\n');
|
|
90
|
+
return [...before, ...replacement, ...after];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Rename a requirement header in the lines array.
|
|
94
|
+
* Returns a new array with the header renamed.
|
|
95
|
+
*/
|
|
96
|
+
function renameBlock(lines, fromName, toName) {
|
|
97
|
+
return lines.map((line) => {
|
|
98
|
+
const match = REQUIREMENT_HEADER.exec(line);
|
|
99
|
+
if (match && match[1].trim() === fromName) {
|
|
100
|
+
return `### Requirement: ${toName}`;
|
|
101
|
+
}
|
|
102
|
+
return line;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Append a requirement block to the end of the lines array.
|
|
107
|
+
*/
|
|
108
|
+
function appendBlock(lines, block) {
|
|
109
|
+
const trimmed = trimTrailingBlanks([...lines]);
|
|
110
|
+
const markdown = blockToMarkdown(block);
|
|
111
|
+
return [...trimmed, '', ...markdown.split('\n')];
|
|
112
|
+
}
|
|
113
|
+
function trimTrailingBlanks(lines) {
|
|
114
|
+
const result = [...lines];
|
|
115
|
+
while (result.length > 0 && result[result.length - 1].trim() === '') {
|
|
116
|
+
result.pop();
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function trimLeadingBlanks(lines) {
|
|
121
|
+
const result = [...lines];
|
|
122
|
+
while (result.length > 0 && result[0].trim() === '') {
|
|
123
|
+
result.shift();
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Public API
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Apply a delta spec to a main spec.
|
|
132
|
+
*
|
|
133
|
+
* Operations are applied in this order:
|
|
134
|
+
* 1. RENAMED - rename requirement headers
|
|
135
|
+
* 2. REMOVED - delete requirement blocks
|
|
136
|
+
* 3. MODIFIED - replace requirement blocks
|
|
137
|
+
* 4. ADDED - append new requirement blocks
|
|
138
|
+
*
|
|
139
|
+
* @param mainSpecContent - The raw markdown of the main spec
|
|
140
|
+
* @param deltaContent - The raw markdown of the delta spec
|
|
141
|
+
* @returns The updated main spec markdown
|
|
142
|
+
* @throws When a MODIFIED target does not exist in the main spec
|
|
143
|
+
*/
|
|
144
|
+
export function applyDeltaSpec(mainSpecContent, deltaContent) {
|
|
145
|
+
const delta = parseDeltaSpec(deltaContent);
|
|
146
|
+
const normalized = mainSpecContent.replace(/\r\n?/g, '\n');
|
|
147
|
+
let lines = normalized.split('\n');
|
|
148
|
+
// 1. Apply RENAMED
|
|
149
|
+
for (const rename of delta.renamed) {
|
|
150
|
+
lines = renameBlock(lines, rename.from, rename.to);
|
|
151
|
+
}
|
|
152
|
+
// 2. Apply REMOVED
|
|
153
|
+
for (const removed of delta.removed) {
|
|
154
|
+
lines = removeBlock(lines, removed.name);
|
|
155
|
+
}
|
|
156
|
+
// 3. Apply MODIFIED
|
|
157
|
+
for (const modified of delta.modified) {
|
|
158
|
+
lines = replaceBlock(lines, modified.name, modified);
|
|
159
|
+
}
|
|
160
|
+
// 4. Apply ADDED
|
|
161
|
+
for (const added of delta.added) {
|
|
162
|
+
lines = appendBlock(lines, added);
|
|
163
|
+
}
|
|
164
|
+
return trimLeadingBlanks(trimTrailingBlanks([...lines])).join('\n') + '\n';
|
|
165
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TemplateId } from './types.js';
|
|
2
|
+
export type { TemplateId } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Error thrown when a template cannot be found or loaded.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TemplateNotFoundError extends Error {
|
|
7
|
+
readonly templateId: string;
|
|
8
|
+
readonly templatePath: string;
|
|
9
|
+
constructor(templateId: string, templatePath: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Returns the absolute path to a template file.
|
|
13
|
+
*
|
|
14
|
+
* @param id - The template identifier
|
|
15
|
+
* @returns Absolute path to the template markdown file
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTemplatePath(id: TemplateId): string;
|
|
18
|
+
/**
|
|
19
|
+
* Loads a template file and returns its contents as a string.
|
|
20
|
+
*
|
|
21
|
+
* @param id - The template identifier
|
|
22
|
+
* @returns The template content
|
|
23
|
+
* @throws TemplateNotFoundError if the template file does not exist
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadTemplate(id: TemplateId): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the list of all available template IDs.
|
|
28
|
+
*
|
|
29
|
+
* @returns Array of valid template identifiers
|
|
30
|
+
*/
|
|
31
|
+
export declare function listTemplates(): TemplateId[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as fsSync from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/**
|
|
6
|
+
* All valid template IDs.
|
|
7
|
+
*/
|
|
8
|
+
const TEMPLATE_IDS = [
|
|
9
|
+
'proposal',
|
|
10
|
+
'spec',
|
|
11
|
+
'design',
|
|
12
|
+
'tasks',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Error thrown when a template cannot be found or loaded.
|
|
16
|
+
*/
|
|
17
|
+
export class TemplateNotFoundError extends Error {
|
|
18
|
+
templateId;
|
|
19
|
+
templatePath;
|
|
20
|
+
constructor(templateId, templatePath) {
|
|
21
|
+
super(`Template "${templateId}" not found at ${templatePath}`);
|
|
22
|
+
this.templateId = templateId;
|
|
23
|
+
this.templatePath = templatePath;
|
|
24
|
+
this.name = 'TemplateNotFoundError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the package root by walking up from the current module until
|
|
29
|
+
* a directory containing package.json is found.
|
|
30
|
+
*/
|
|
31
|
+
function findPackageRoot() {
|
|
32
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
33
|
+
let dir = path.dirname(currentFile);
|
|
34
|
+
const root = path.parse(dir).root;
|
|
35
|
+
while (dir !== root) {
|
|
36
|
+
if (fsSync.existsSync(path.join(dir, 'package.json'))) {
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
dir = path.dirname(dir);
|
|
40
|
+
}
|
|
41
|
+
throw new Error('Could not find package root (no package.json found in parent directories)');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns the absolute path to a template file.
|
|
45
|
+
*
|
|
46
|
+
* @param id - The template identifier
|
|
47
|
+
* @returns Absolute path to the template markdown file
|
|
48
|
+
*/
|
|
49
|
+
export function getTemplatePath(id) {
|
|
50
|
+
const packageRoot = findPackageRoot();
|
|
51
|
+
return path.join(packageRoot, 'templates', `${id}.md`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Loads a template file and returns its contents as a string.
|
|
55
|
+
*
|
|
56
|
+
* @param id - The template identifier
|
|
57
|
+
* @returns The template content
|
|
58
|
+
* @throws TemplateNotFoundError if the template file does not exist
|
|
59
|
+
*/
|
|
60
|
+
export async function loadTemplate(id) {
|
|
61
|
+
const templatePath = getTemplatePath(id);
|
|
62
|
+
try {
|
|
63
|
+
return await fs.readFile(templatePath, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
67
|
+
throw new TemplateNotFoundError(id, templatePath);
|
|
68
|
+
}
|
|
69
|
+
const readError = err instanceof Error ? err : new Error(String(err));
|
|
70
|
+
throw new Error(`Failed to load template "${id}": ${readError.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns the list of all available template IDs.
|
|
75
|
+
*
|
|
76
|
+
* @returns Array of valid template identifiers
|
|
77
|
+
*/
|
|
78
|
+
export function listTemplates() {
|
|
79
|
+
return [...TEMPLATE_IDS];
|
|
80
|
+
}
|