@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.
Files changed (178) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +442 -0
  3. package/bin/specflow.js +2 -0
  4. package/dist/cli/commands/change-archive.d.ts +26 -0
  5. package/dist/cli/commands/change-archive.js +50 -0
  6. package/dist/cli/commands/change-new.d.ts +18 -0
  7. package/dist/cli/commands/change-new.js +53 -0
  8. package/dist/cli/commands/change-phase.d.ts +33 -0
  9. package/dist/cli/commands/change-phase.js +66 -0
  10. package/dist/cli/commands/change-status.d.ts +35 -0
  11. package/dist/cli/commands/change-status.js +95 -0
  12. package/dist/cli/commands/doctor.d.ts +9 -0
  13. package/dist/cli/commands/doctor.js +48 -0
  14. package/dist/cli/commands/init.d.ts +16 -0
  15. package/dist/cli/commands/init.js +122 -0
  16. package/dist/cli/commands/instructions.d.ts +21 -0
  17. package/dist/cli/commands/instructions.js +75 -0
  18. package/dist/cli/commands/parity-report.d.ts +10 -0
  19. package/dist/cli/commands/parity-report.js +67 -0
  20. package/dist/cli/commands/sync.d.ts +4 -0
  21. package/dist/cli/commands/sync.js +40 -0
  22. package/dist/cli/commands/validate.d.ts +21 -0
  23. package/dist/cli/commands/validate.js +46 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +52 -0
  26. package/dist/core/archive.d.ts +39 -0
  27. package/dist/core/archive.js +154 -0
  28. package/dist/core/artifact-graph/graph.d.ts +60 -0
  29. package/dist/core/artifact-graph/graph.js +141 -0
  30. package/dist/core/artifact-graph/index.d.ts +10 -0
  31. package/dist/core/artifact-graph/index.js +7 -0
  32. package/dist/core/artifact-graph/instruction-loader.d.ts +45 -0
  33. package/dist/core/artifact-graph/instruction-loader.js +37 -0
  34. package/dist/core/artifact-graph/outputs.d.ts +35 -0
  35. package/dist/core/artifact-graph/outputs.js +48 -0
  36. package/dist/core/artifact-graph/resolver.d.ts +54 -0
  37. package/dist/core/artifact-graph/resolver.js +139 -0
  38. package/dist/core/artifact-graph/schema.d.ts +23 -0
  39. package/dist/core/artifact-graph/schema.js +115 -0
  40. package/dist/core/artifact-graph/state.d.ts +9 -0
  41. package/dist/core/artifact-graph/state.js +93 -0
  42. package/dist/core/artifact-graph/types.d.ts +123 -0
  43. package/dist/core/artifact-graph/types.js +29 -0
  44. package/dist/core/parsers/change-parser.d.ts +20 -0
  45. package/dist/core/parsers/change-parser.js +22 -0
  46. package/dist/core/parsers/index.d.ts +7 -0
  47. package/dist/core/parsers/index.js +4 -0
  48. package/dist/core/parsers/markdown-parser.d.ts +35 -0
  49. package/dist/core/parsers/markdown-parser.js +193 -0
  50. package/dist/core/parsers/requirement-blocks.d.ts +21 -0
  51. package/dist/core/parsers/requirement-blocks.js +57 -0
  52. package/dist/core/parsers/spec-structure.d.ts +22 -0
  53. package/dist/core/parsers/spec-structure.js +105 -0
  54. package/dist/core/specs-apply.d.ts +22 -0
  55. package/dist/core/specs-apply.js +165 -0
  56. package/dist/core/templates/index.d.ts +31 -0
  57. package/dist/core/templates/index.js +80 -0
  58. package/dist/core/templates/types.d.ts +4 -0
  59. package/dist/core/templates/types.js +1 -0
  60. package/dist/core/validation/constants.d.ts +14 -0
  61. package/dist/core/validation/constants.js +14 -0
  62. package/dist/core/validation/types.d.ts +13 -0
  63. package/dist/core/validation/types.js +6 -0
  64. package/dist/core/validation/validator.d.ts +24 -0
  65. package/dist/core/validation/validator.js +250 -0
  66. package/dist/integrations/claude/adapter.d.ts +2 -0
  67. package/dist/integrations/claude/adapter.js +106 -0
  68. package/dist/integrations/codex/adapter.d.ts +2 -0
  69. package/dist/integrations/codex/adapter.js +141 -0
  70. package/dist/integrations/cursor/adapter.d.ts +2 -0
  71. package/dist/integrations/cursor/adapter.js +126 -0
  72. package/dist/integrations/shared/adapter-registry.d.ts +4 -0
  73. package/dist/integrations/shared/adapter-registry.js +27 -0
  74. package/dist/integrations/shared/asset-copy.d.ts +2 -0
  75. package/dist/integrations/shared/asset-copy.js +44 -0
  76. package/dist/integrations/shared/asset-hash.d.ts +4 -0
  77. package/dist/integrations/shared/asset-hash.js +54 -0
  78. package/dist/integrations/shared/capability-evidence.d.ts +1 -0
  79. package/dist/integrations/shared/capability-evidence.js +42 -0
  80. package/dist/integrations/shared/command-catalog.d.ts +6 -0
  81. package/dist/integrations/shared/command-catalog.js +13 -0
  82. package/dist/integrations/shared/managed-assets.d.ts +2 -0
  83. package/dist/integrations/shared/managed-assets.js +21 -0
  84. package/dist/integrations/shared/marker-write.d.ts +3 -0
  85. package/dist/integrations/shared/marker-write.js +48 -0
  86. package/dist/integrations/shared/migration-state.d.ts +2 -0
  87. package/dist/integrations/shared/migration-state.js +67 -0
  88. package/dist/integrations/shared/parity-comparator.d.ts +6 -0
  89. package/dist/integrations/shared/parity-comparator.js +97 -0
  90. package/dist/integrations/shared/parity-manifest.d.ts +13 -0
  91. package/dist/integrations/shared/parity-manifest.js +58 -0
  92. package/dist/integrations/shared/runtime-assets.d.ts +2 -0
  93. package/dist/integrations/shared/runtime-assets.js +21 -0
  94. package/dist/integrations/shared/skill-renderer.d.ts +5 -0
  95. package/dist/integrations/shared/skill-renderer.js +77 -0
  96. package/dist/integrations/shared/types.d.ts +35 -0
  97. package/dist/integrations/shared/types.js +1 -0
  98. package/dist/utils/change-metadata.d.ts +38 -0
  99. package/dist/utils/change-metadata.js +82 -0
  100. package/dist/utils/change-utils.d.ts +53 -0
  101. package/dist/utils/change-utils.js +97 -0
  102. package/dist/utils/file-system.d.ts +35 -0
  103. package/dist/utils/file-system.js +67 -0
  104. package/dist/utils/index.d.ts +7 -0
  105. package/dist/utils/index.js +5 -0
  106. package/dist/utils/project-root.d.ts +22 -0
  107. package/dist/utils/project-root.js +44 -0
  108. package/dist/utils/task-progress.d.ts +39 -0
  109. package/dist/utils/task-progress.js +62 -0
  110. package/package.json +72 -0
  111. package/prompts/build/ecc-go-reviewer.md +80 -0
  112. package/prompts/build/ecc-java-reviewer.md +185 -0
  113. package/prompts/build/ecc-kotlin-reviewer.md +163 -0
  114. package/prompts/build/ecc-python-reviewer.md +102 -0
  115. package/prompts/build/ecc-rust-reviewer.md +98 -0
  116. package/prompts/build/ecc-typescript-reviewer.md +116 -0
  117. package/prompts/build/language-router.md +22 -0
  118. package/prompts/build/phase-a-plan.md +288 -0
  119. package/prompts/build/phase-b-execute.md +276 -0
  120. package/prompts/build/phase-b-review.md +119 -0
  121. package/prompts/build/phase-b-worktree.md +215 -0
  122. package/prompts/build/plan-document-reviewer.md +51 -0
  123. package/prompts/build/tdd.md +184 -0
  124. package/prompts/done/branch-finish.md +204 -0
  125. package/prompts/fix/debug.md +249 -0
  126. package/prompts/plan/design-draft.md +123 -0
  127. package/prompts/plan/proposal.md +85 -0
  128. package/prompts/plan/specs.md +114 -0
  129. package/prompts/plan/tasks-draft.md +123 -0
  130. package/prompts/reference/openspec/apply-instruction.md +2 -0
  131. package/prompts/reference/openspec/design-instruction.md +20 -0
  132. package/prompts/reference/openspec/proposal-instruction.md +18 -0
  133. package/prompts/reference/openspec/specs-instruction.md +47 -0
  134. package/prompts/reference/openspec/tasks-instruction.md +26 -0
  135. package/prompts/reference/specflow/example-design.md +284 -0
  136. package/prompts/reference/superpowers/anthropic-best-practices.md +1150 -0
  137. package/prompts/reference/superpowers/codex-tools.md +29 -0
  138. package/prompts/reference/superpowers/condition-based-waiting-example.ts +158 -0
  139. package/prompts/reference/superpowers/condition-based-waiting.md +115 -0
  140. package/prompts/reference/superpowers/defense-in-depth.md +122 -0
  141. package/prompts/reference/superpowers/find-polluter.sh +63 -0
  142. package/prompts/reference/superpowers/graphviz-conventions.dot +172 -0
  143. package/prompts/reference/superpowers/persuasion-principles.md +187 -0
  144. package/prompts/reference/superpowers/root-cause-tracing.md +169 -0
  145. package/prompts/reference/superpowers/testing-anti-patterns.md +299 -0
  146. package/prompts/reference/superpowers/testing-skills-with-subagents.md +384 -0
  147. package/prompts/reference/superpowers/using-superpowers.md +117 -0
  148. package/prompts/reference/superpowers/writing-skills.md +655 -0
  149. package/prompts/refine/brainstorm.md +241 -0
  150. package/prompts/refine/design-output.md +126 -0
  151. package/prompts/refine/spec-document-reviewer.md +51 -0
  152. package/prompts/refine/update-artifacts.md +185 -0
  153. package/prompts/review/code-review.md +223 -0
  154. package/prompts/shared/code-reviewer-prompt.md +98 -0
  155. package/prompts/shared/dispatching-parallel-agents.md +143 -0
  156. package/prompts/shared/executing-plans.md +67 -0
  157. package/prompts/shared/implementer-prompt.md +115 -0
  158. package/prompts/shared/receiving-code-review.md +174 -0
  159. package/prompts/shared/spec-reviewer-prompt.md +63 -0
  160. package/prompts/test/tdd.md +236 -0
  161. package/prompts/test/verification.md +129 -0
  162. package/prompts/verify/ecc-security-reviewer.md +112 -0
  163. package/prompts/verify/verification.md +196 -0
  164. package/schemas/specflow/schema.yaml +48 -0
  165. package/skills/specflow-build/SKILL.md +129 -0
  166. package/skills/specflow-done/SKILL.md +68 -0
  167. package/skills/specflow-fix/SKILL.md +74 -0
  168. package/skills/specflow-plan/SKILL.md +82 -0
  169. package/skills/specflow-refine/SKILL.md +128 -0
  170. package/skills/specflow-review/SKILL.md +40 -0
  171. package/skills/specflow-scan/SKILL.md +48 -0
  172. package/skills/specflow-snap/SKILL.md +46 -0
  173. package/skills/specflow-test/SKILL.md +48 -0
  174. package/skills/specflow-verify/SKILL.md +77 -0
  175. package/templates/design.md +19 -0
  176. package/templates/proposal.md +23 -0
  177. package/templates/spec.md +42 -0
  178. 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
+ }