sysprom 1.0.0 → 1.0.6

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 (148) hide show
  1. package/README.md +207 -0
  2. package/dist/schema.json +510 -0
  3. package/dist/src/canonical-json.d.ts +23 -0
  4. package/dist/src/canonical-json.js +120 -0
  5. package/dist/src/cli/commands/add.d.ts +22 -0
  6. package/dist/src/cli/commands/add.js +95 -0
  7. package/dist/src/cli/commands/check.d.ts +10 -0
  8. package/dist/src/cli/commands/check.js +33 -0
  9. package/dist/src/cli/commands/graph.d.ts +15 -0
  10. package/dist/src/cli/commands/graph.js +32 -0
  11. package/dist/src/cli/commands/init.d.ts +2 -0
  12. package/dist/src/cli/commands/init.js +44 -0
  13. package/dist/src/cli/commands/json2md.d.ts +2 -0
  14. package/dist/src/cli/commands/json2md.js +60 -0
  15. package/dist/src/cli/commands/md2json.d.ts +2 -0
  16. package/dist/src/cli/commands/md2json.js +29 -0
  17. package/dist/src/cli/commands/plan.d.ts +2 -0
  18. package/dist/src/cli/commands/plan.js +227 -0
  19. package/dist/src/cli/commands/query.d.ts +2 -0
  20. package/dist/src/cli/commands/query.js +275 -0
  21. package/dist/src/cli/commands/remove.d.ts +13 -0
  22. package/dist/src/cli/commands/remove.js +50 -0
  23. package/dist/src/cli/commands/rename.d.ts +14 -0
  24. package/dist/src/cli/commands/rename.js +34 -0
  25. package/dist/src/cli/commands/search.d.ts +11 -0
  26. package/dist/src/cli/commands/search.js +37 -0
  27. package/dist/src/cli/commands/speckit.d.ts +2 -0
  28. package/dist/src/cli/commands/speckit.js +318 -0
  29. package/dist/src/cli/commands/stats.d.ts +10 -0
  30. package/dist/src/cli/commands/stats.js +51 -0
  31. package/dist/src/cli/commands/task.d.ts +2 -0
  32. package/dist/src/cli/commands/task.js +162 -0
  33. package/dist/src/cli/commands/update.d.ts +2 -0
  34. package/dist/src/cli/commands/update.js +219 -0
  35. package/dist/src/cli/commands/validate.d.ts +10 -0
  36. package/dist/src/cli/commands/validate.js +30 -0
  37. package/dist/src/cli/define-command.d.ts +34 -0
  38. package/dist/src/cli/define-command.js +237 -0
  39. package/dist/src/cli/index.d.ts +2 -0
  40. package/dist/src/cli/index.js +3 -0
  41. package/dist/src/cli/program.d.ts +4 -0
  42. package/dist/src/cli/program.js +46 -0
  43. package/dist/src/cli/shared.d.ts +26 -0
  44. package/dist/src/cli/shared.js +41 -0
  45. package/dist/src/generate-schema.d.ts +1 -0
  46. package/dist/src/generate-schema.js +9 -0
  47. package/dist/src/index.d.ts +48 -0
  48. package/dist/src/index.js +99 -0
  49. package/dist/src/io.d.ts +22 -0
  50. package/dist/src/io.js +66 -0
  51. package/dist/src/json-to-md.d.ts +26 -0
  52. package/dist/src/json-to-md.js +498 -0
  53. package/dist/src/md-to-json.d.ts +22 -0
  54. package/dist/src/md-to-json.js +548 -0
  55. package/dist/src/operations/add-node.d.ts +887 -0
  56. package/dist/src/operations/add-node.js +21 -0
  57. package/dist/src/operations/add-plan-task.d.ts +594 -0
  58. package/dist/src/operations/add-plan-task.js +25 -0
  59. package/dist/src/operations/add-relationship.d.ts +635 -0
  60. package/dist/src/operations/add-relationship.js +25 -0
  61. package/dist/src/operations/check.d.ts +301 -0
  62. package/dist/src/operations/check.js +66 -0
  63. package/dist/src/operations/define-operation.d.ts +14 -0
  64. package/dist/src/operations/define-operation.js +21 -0
  65. package/dist/src/operations/graph.d.ts +303 -0
  66. package/dist/src/operations/graph.js +71 -0
  67. package/dist/src/operations/index.d.ts +38 -0
  68. package/dist/src/operations/index.js +45 -0
  69. package/dist/src/operations/init-document.d.ts +299 -0
  70. package/dist/src/operations/init-document.js +26 -0
  71. package/dist/src/operations/json-to-markdown.d.ts +298 -0
  72. package/dist/src/operations/json-to-markdown.js +13 -0
  73. package/dist/src/operations/mark-task-done.d.ts +594 -0
  74. package/dist/src/operations/mark-task-done.js +26 -0
  75. package/dist/src/operations/mark-task-undone.d.ts +594 -0
  76. package/dist/src/operations/mark-task-undone.js +26 -0
  77. package/dist/src/operations/markdown-to-json.d.ts +298 -0
  78. package/dist/src/operations/markdown-to-json.js +13 -0
  79. package/dist/src/operations/next-id.d.ts +322 -0
  80. package/dist/src/operations/next-id.js +29 -0
  81. package/dist/src/operations/node-history.d.ts +313 -0
  82. package/dist/src/operations/node-history.js +55 -0
  83. package/dist/src/operations/plan-add-task.d.ts +595 -0
  84. package/dist/src/operations/plan-add-task.js +18 -0
  85. package/dist/src/operations/plan-gate.d.ts +351 -0
  86. package/dist/src/operations/plan-gate.js +41 -0
  87. package/dist/src/operations/plan-init.d.ts +299 -0
  88. package/dist/src/operations/plan-init.js +17 -0
  89. package/dist/src/operations/plan-progress.d.ts +313 -0
  90. package/dist/src/operations/plan-progress.js +23 -0
  91. package/dist/src/operations/plan-status.d.ts +349 -0
  92. package/dist/src/operations/plan-status.js +41 -0
  93. package/dist/src/operations/query-node.d.ts +1065 -0
  94. package/dist/src/operations/query-node.js +27 -0
  95. package/dist/src/operations/query-nodes.d.ts +594 -0
  96. package/dist/src/operations/query-nodes.js +23 -0
  97. package/dist/src/operations/query-relationships.d.ts +343 -0
  98. package/dist/src/operations/query-relationships.js +27 -0
  99. package/dist/src/operations/remove-node.d.ts +895 -0
  100. package/dist/src/operations/remove-node.js +58 -0
  101. package/dist/src/operations/remove-relationship.d.ts +622 -0
  102. package/dist/src/operations/remove-relationship.js +26 -0
  103. package/dist/src/operations/rename.d.ts +594 -0
  104. package/dist/src/operations/rename.js +113 -0
  105. package/dist/src/operations/search.d.ts +593 -0
  106. package/dist/src/operations/search.js +39 -0
  107. package/dist/src/operations/speckit-diff.d.ts +330 -0
  108. package/dist/src/operations/speckit-diff.js +89 -0
  109. package/dist/src/operations/speckit-export.d.ts +300 -0
  110. package/dist/src/operations/speckit-export.js +17 -0
  111. package/dist/src/operations/speckit-import.d.ts +299 -0
  112. package/dist/src/operations/speckit-import.js +39 -0
  113. package/dist/src/operations/speckit-sync.d.ts +900 -0
  114. package/dist/src/operations/speckit-sync.js +116 -0
  115. package/dist/src/operations/state-at.d.ts +309 -0
  116. package/dist/src/operations/state-at.js +53 -0
  117. package/dist/src/operations/stats.d.ts +324 -0
  118. package/dist/src/operations/stats.js +85 -0
  119. package/dist/src/operations/task-list.d.ts +305 -0
  120. package/dist/src/operations/task-list.js +44 -0
  121. package/dist/src/operations/timeline.d.ts +312 -0
  122. package/dist/src/operations/timeline.js +46 -0
  123. package/dist/src/operations/trace-from-node.d.ts +1197 -0
  124. package/dist/src/operations/trace-from-node.js +36 -0
  125. package/dist/src/operations/update-metadata.d.ts +593 -0
  126. package/dist/src/operations/update-metadata.js +18 -0
  127. package/dist/src/operations/update-node.d.ts +957 -0
  128. package/dist/src/operations/update-node.js +24 -0
  129. package/dist/src/operations/update-plan-task.d.ts +595 -0
  130. package/dist/src/operations/update-plan-task.js +31 -0
  131. package/dist/src/operations/validate.d.ts +310 -0
  132. package/dist/src/operations/validate.js +82 -0
  133. package/dist/src/schema.d.ts +891 -0
  134. package/dist/src/schema.js +356 -0
  135. package/dist/src/speckit/generate.d.ts +7 -0
  136. package/dist/src/speckit/generate.js +546 -0
  137. package/dist/src/speckit/index.d.ts +4 -0
  138. package/dist/src/speckit/index.js +4 -0
  139. package/dist/src/speckit/parse.d.ts +11 -0
  140. package/dist/src/speckit/parse.js +712 -0
  141. package/dist/src/speckit/plan.d.ts +125 -0
  142. package/dist/src/speckit/plan.js +636 -0
  143. package/dist/src/speckit/project.d.ts +39 -0
  144. package/dist/src/speckit/project.js +141 -0
  145. package/dist/src/text.d.ts +23 -0
  146. package/dist/src/text.js +32 -0
  147. package/package.json +86 -8
  148. package/schema.json +510 -0
@@ -0,0 +1,712 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join, basename } from "node:path";
3
+ // ---------------------------------------------------------------------------
4
+ // Helper functions — markdown parsing
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Parse markdown content into a hierarchical section tree by heading level.
8
+ */
9
+ function parseSections(body) {
10
+ const lines = body.split("\n");
11
+ const all = [];
12
+ // First pass: find all headings and collect their body text
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const hMatch = /^(#{1,6})\s+(.+)$/.exec(lines[i]);
15
+ if (hMatch) {
16
+ const level = hMatch[1].length;
17
+ const heading = hMatch[2];
18
+ const bodyLines = [];
19
+ // Collect lines until the next heading
20
+ for (let j = i + 1; j < lines.length; j++) {
21
+ if (/^#{1,6}\s/.exec(lines[j]))
22
+ break;
23
+ bodyLines.push(lines[j]);
24
+ }
25
+ all.push({
26
+ level,
27
+ heading,
28
+ body: bodyLines.join("\n").trim(),
29
+ children: [],
30
+ });
31
+ }
32
+ }
33
+ // Second pass: build tree structure
34
+ const root = [];
35
+ const stack = [];
36
+ for (const section of all) {
37
+ while (stack.length > 0 && stack[stack.length - 1].level >= section.level) {
38
+ stack.pop();
39
+ }
40
+ if (stack.length > 0) {
41
+ stack[stack.length - 1].children.push(section);
42
+ }
43
+ else {
44
+ root.push(section);
45
+ }
46
+ stack.push(section);
47
+ }
48
+ return root;
49
+ }
50
+ /**
51
+ * Extract bold key-value pairs from markdown like "**Key**: value" or "**Key**: value text".
52
+ */
53
+ function parseFrontMatterish(content) {
54
+ const result = {};
55
+ const lines = content.split("\n");
56
+ for (const line of lines) {
57
+ const match = /^\*\*([^*]+)\*\*:\s*(.+)$/.exec(line);
58
+ if (match) {
59
+ const [, key, value] = match;
60
+ result[key] = value.trim();
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ /**
66
+ * Parse checkbox lines like "- [x] ID text" or "- [ ] ID text".
67
+ */
68
+ function parseCheckboxes(body) {
69
+ const items = [];
70
+ const lines = body.split("\n");
71
+ for (const line of lines) {
72
+ const match = /^-\s+\[([x ])\]\s+(.+)$/.exec(line);
73
+ if (match) {
74
+ const [, checkbox, text] = match;
75
+ const done = checkbox === "x";
76
+ // Extract ID as the first token (e.g., "T001", "CHK001")
77
+ const textMatch = /^(\S+)\s+(.*)$/.exec(text);
78
+ const id = textMatch ? textMatch[1] : text;
79
+ const itemText = textMatch ? textMatch[2] : text;
80
+ items.push({ id, text: itemText, done });
81
+ }
82
+ }
83
+ return items;
84
+ }
85
+ /**
86
+ * Flatten all sections in the tree into a single array for easier searching.
87
+ */
88
+ function flattenSections(sections) {
89
+ const result = [];
90
+ function walk(s) {
91
+ result.push(s);
92
+ for (const c of s.children)
93
+ walk(c);
94
+ }
95
+ for (const s of sections)
96
+ walk(s);
97
+ return result;
98
+ }
99
+ /**
100
+ * Find the first section whose heading matches a predicate (searches entire tree).
101
+ */
102
+ function findSection(sections, predicate) {
103
+ return flattenSections(sections).find((s) => predicate(s.heading));
104
+ }
105
+ /**
106
+ * Convert status-like strings to NodeStatus. Recognizes common spec-kit patterns.
107
+ */
108
+ function mapStatusValue(value) {
109
+ const lower = value.toLowerCase().trim();
110
+ const statusMap = {
111
+ draft: "proposed",
112
+ proposed: "proposed",
113
+ accepted: "accepted",
114
+ active: "active",
115
+ implemented: "implemented",
116
+ adopted: "adopted",
117
+ defined: "defined",
118
+ introduced: "introduced",
119
+ in_progress: "in_progress",
120
+ complete: "complete",
121
+ consolidated: "consolidated",
122
+ experimental: "experimental",
123
+ deprecated: "deprecated",
124
+ retired: "retired",
125
+ superseded: "superseded",
126
+ abandoned: "abandoned",
127
+ deferred: "deferred",
128
+ };
129
+ return statusMap[lower] ?? "proposed";
130
+ }
131
+ /**
132
+ * Extract a single line value from body text (e.g., "**Created**: 2025-01-01").
133
+ */
134
+ function extractValue(body, key) {
135
+ const pattern = new RegExp(`^\\*\\*${key}\\*\\*:\\s*(.+)$`, "m");
136
+ const match = body.match(pattern);
137
+ return match ? match[1].trim() : undefined;
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // constitution.md parser
141
+ // ---------------------------------------------------------------------------
142
+ export function parseConstitution(content, idPrefix) {
143
+ const sections = parseSections(content);
144
+ const nodes = [];
145
+ const relationships = [];
146
+ // Find title (first # heading)
147
+ let title = "Constitution";
148
+ const titleSection = sections.find((s) => s.level === 1);
149
+ if (titleSection) {
150
+ title = titleSection.heading;
151
+ }
152
+ // Create protocol node for the constitution
153
+ const protocolId = `${idPrefix}-CONST`;
154
+ nodes.push({
155
+ id: protocolId,
156
+ type: "protocol",
157
+ name: title,
158
+ description: titleSection?.body,
159
+ });
160
+ // Search entire tree for sections
161
+ const allSections = flattenSections(sections);
162
+ // Find "Core Principles" section and extract invariants
163
+ let principlesIdx = 0;
164
+ const principlesSection = findSection(sections, (h) => h.toLowerCase().includes("core principles") ||
165
+ h.toLowerCase() === "principles");
166
+ if (principlesSection) {
167
+ for (const child of principlesSection.children) {
168
+ principlesIdx++;
169
+ const invariantId = `${idPrefix}-INV-${String(principlesIdx)}`;
170
+ nodes.push({
171
+ id: invariantId,
172
+ type: "invariant",
173
+ name: child.heading,
174
+ description: child.body,
175
+ });
176
+ relationships.push({
177
+ from: invariantId,
178
+ to: protocolId,
179
+ type: "part_of",
180
+ });
181
+ }
182
+ }
183
+ // Find "Governance" section and create policy node
184
+ const govSection = findSection(sections, (h) => h.toLowerCase() === "governance");
185
+ if (govSection) {
186
+ const govPolicyId = `${idPrefix}-POL-GOV`;
187
+ nodes.push({
188
+ id: govPolicyId,
189
+ type: "policy",
190
+ name: "Governance",
191
+ description: govSection.body,
192
+ });
193
+ relationships.push({
194
+ from: govPolicyId,
195
+ to: protocolId,
196
+ type: "part_of",
197
+ });
198
+ }
199
+ // Other ## sections become policies (excluding the main title and known sections)
200
+ const knownHeadings = new Set([
201
+ "core principles",
202
+ "principles",
203
+ "governance",
204
+ ]);
205
+ let policyIdx = 1;
206
+ for (const section of allSections) {
207
+ if (section.level === 2 &&
208
+ !knownHeadings.has(section.heading.toLowerCase()) &&
209
+ !section.heading.toLowerCase().includes("core principles")) {
210
+ const policyId = `${idPrefix}-POL-${String(policyIdx)}`;
211
+ nodes.push({
212
+ id: policyId,
213
+ type: "policy",
214
+ name: section.heading,
215
+ description: section.body,
216
+ });
217
+ relationships.push({
218
+ from: policyId,
219
+ to: protocolId,
220
+ type: "part_of",
221
+ });
222
+ policyIdx++;
223
+ }
224
+ }
225
+ return { nodes, relationships };
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // spec.md parser
229
+ // ---------------------------------------------------------------------------
230
+ export function parseSpec(content, idPrefix) {
231
+ const sections = parseSections(content);
232
+ const allSections = flattenSections(sections);
233
+ const nodes = [];
234
+ const relationships = [];
235
+ // Extract metadata from content (bold key-value pairs)
236
+ const statusStr = extractValue(content, "Status") ?? "Draft";
237
+ const status = mapStatusValue(statusStr);
238
+ // Find title (first # heading)
239
+ let title = "Specification";
240
+ const titleSection = sections.find((s) => s.level === 1);
241
+ if (titleSection) {
242
+ title = titleSection.heading.replace(/^Feature Specification:\s*/i, "");
243
+ }
244
+ // Create artefact node for the spec
245
+ const specId = `${idPrefix}-SPEC`;
246
+ nodes.push({
247
+ id: specId,
248
+ type: "artefact",
249
+ name: title,
250
+ status,
251
+ description: titleSection?.body,
252
+ });
253
+ // Track user stories, FRs, SCs, and entities
254
+ let usIdx = 0;
255
+ let frIdx = 0;
256
+ let scIdx = 0;
257
+ let entityIdx = 0;
258
+ // Find "User Scenarios & Testing" section
259
+ for (const section of allSections) {
260
+ if (section.heading.toLowerCase().includes("user scenarios") ||
261
+ section.heading.toLowerCase().includes("user story")) {
262
+ // Each "### User Story N - Title (Priority: PN)" becomes a capability
263
+ for (const child of section.children) {
264
+ const usMatch = /^User Story\s+(\d+)\s*-\s*(.+?)\s*\(Priority:\s*P(\d+)\)/i.exec(child.heading);
265
+ if (usMatch) {
266
+ usIdx++;
267
+ const capabilityId = `${idPrefix}-US-${String(usIdx)}`;
268
+ const priority = `P${usMatch[3]}`;
269
+ const storyName = usMatch[2];
270
+ // Extract acceptance scenarios
271
+ const acceptanceLines = [];
272
+ let inAcceptance = false;
273
+ for (const line of child.body.split("\n")) {
274
+ if (line.toLowerCase().includes("acceptance scenario")) {
275
+ inAcceptance = true;
276
+ continue;
277
+ }
278
+ if (inAcceptance && /^#+\s/.exec(line))
279
+ break;
280
+ if (inAcceptance) {
281
+ acceptanceLines.push(line);
282
+ }
283
+ }
284
+ // Look for "Independent Test"
285
+ let independentTest;
286
+ const testMatch = /\*\*Independent Test\*\*:\s*(.+?)(?:\n|$)/.exec(child.body);
287
+ if (testMatch) {
288
+ independentTest = testMatch[1];
289
+ }
290
+ const description = [];
291
+ description.push(`Priority: ${priority}`);
292
+ if (acceptanceLines.length > 0) {
293
+ description.push("Acceptance Scenarios:", ...acceptanceLines);
294
+ }
295
+ nodes.push({
296
+ id: capabilityId,
297
+ type: "capability",
298
+ name: storyName,
299
+ description,
300
+ context: independentTest,
301
+ });
302
+ relationships.push({
303
+ from: capabilityId,
304
+ to: specId,
305
+ type: "refines",
306
+ });
307
+ }
308
+ }
309
+ }
310
+ }
311
+ // Find "Requirements" section for FR and entity definitions
312
+ for (const section of allSections) {
313
+ if (section.heading.toLowerCase().includes("requirements")) {
314
+ for (const child of section.children) {
315
+ // Look for "Functional Requirements" subsection
316
+ if (child.heading.toLowerCase().includes("functional")) {
317
+ for (const line of child.body.split("\n")) {
318
+ const frMatch = /^-?\s*\*\*FR-(\d+)\*\*:\s*(.+?)(?:\s*\[NEEDS CLARIFICATION[^\]]*\])?(.*)$/.exec(line);
319
+ if (frMatch) {
320
+ frIdx++;
321
+ const frId = `${idPrefix}-FR-${String(frIdx)}`;
322
+ const frText = frMatch[2] + frMatch[3];
323
+ const needsClarification = line.includes("NEEDS CLARIFICATION");
324
+ nodes.push({
325
+ id: frId,
326
+ type: "invariant",
327
+ name: `FR-${String(frIdx)}`,
328
+ description: frText,
329
+ status: needsClarification ? "proposed" : "active",
330
+ });
331
+ relationships.push({
332
+ from: frId,
333
+ to: specId,
334
+ type: "constrained_by",
335
+ });
336
+ }
337
+ }
338
+ }
339
+ // Look for "Key Entities" subsection
340
+ if (child.heading.toLowerCase().includes("entities")) {
341
+ for (const line of child.body.split("\n")) {
342
+ const entityMatch = /^-?\s*\*\*([^*]+)\*\*:\s*(.+)$/.exec(line);
343
+ if (entityMatch) {
344
+ entityIdx++;
345
+ const entityId = `${idPrefix}-ENT-${String(entityIdx)}`;
346
+ const entityName = entityMatch[1];
347
+ const entityDesc = entityMatch[2];
348
+ nodes.push({
349
+ id: entityId,
350
+ type: "concept",
351
+ name: entityName,
352
+ description: entityDesc,
353
+ });
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ // Find "Success Criteria" section
361
+ for (const section of allSections) {
362
+ if (section.heading.toLowerCase().includes("success criteria")) {
363
+ for (const child of section.children) {
364
+ if (child.heading.toLowerCase().includes("measurable")) {
365
+ for (const line of child.body.split("\n")) {
366
+ const scMatch = /^-?\s*\*\*SC-(\d+)\*\*:\s*(.+)$/.exec(line);
367
+ if (scMatch) {
368
+ scIdx++;
369
+ const scId = `${idPrefix}-SC-${String(scIdx)}`;
370
+ const scText = scMatch[2];
371
+ nodes.push({
372
+ id: scId,
373
+ type: "invariant",
374
+ name: `SC-${String(scIdx)}`,
375
+ description: scText,
376
+ status: "active",
377
+ });
378
+ relationships.push({
379
+ from: scId,
380
+ to: specId,
381
+ type: "constrained_by",
382
+ });
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ // Find "Edge Cases" section and attach to spec as description supplement
390
+ for (const section of allSections) {
391
+ if (section.heading.toLowerCase().includes("edge case")) {
392
+ // Append edge cases to spec description
393
+ if (Array.isArray(nodes[0].description)) {
394
+ nodes[0].description.push("Edge Cases:", section.body);
395
+ }
396
+ else if (nodes[0].description) {
397
+ nodes[0].description = [
398
+ nodes[0].description,
399
+ "Edge Cases:",
400
+ section.body,
401
+ ];
402
+ }
403
+ else {
404
+ nodes[0].description = ["Edge Cases:", section.body];
405
+ }
406
+ }
407
+ }
408
+ return { nodes, relationships };
409
+ }
410
+ // ---------------------------------------------------------------------------
411
+ // plan.md parser
412
+ // ---------------------------------------------------------------------------
413
+ export function parsePlan(content, idPrefix) {
414
+ const sections = parseSections(content);
415
+ const allSections = flattenSections(sections);
416
+ const nodes = [];
417
+ const relationships = [];
418
+ // Find title (first # heading)
419
+ let title = "Implementation Plan";
420
+ const titleSection = sections.find((s) => s.level === 1);
421
+ if (titleSection) {
422
+ title = titleSection.heading.replace(/^Implementation Plan:\s*/i, "");
423
+ }
424
+ // Create plan artefact
425
+ const planId = `${idPrefix}-PLAN`;
426
+ nodes.push({
427
+ id: planId,
428
+ type: "artefact",
429
+ name: title,
430
+ });
431
+ // Extract summary (indexed as nodes[0] below)
432
+ for (const section of allSections) {
433
+ if (section.heading.toLowerCase() === "summary") {
434
+ nodes[0].description = section.body;
435
+ break;
436
+ }
437
+ }
438
+ // Create technical context element
439
+ for (const section of allSections) {
440
+ if (section.heading.toLowerCase().includes("technical context")) {
441
+ const techId = `${idPrefix}-TECH`;
442
+ const contextLines = [];
443
+ for (const line of section.body.split("\n")) {
444
+ if (line.startsWith("- ")) {
445
+ contextLines.push(line.slice(2));
446
+ }
447
+ }
448
+ nodes.push({
449
+ id: techId,
450
+ type: "element",
451
+ name: "Technical Context",
452
+ description: contextLines.length > 0 ? contextLines : undefined,
453
+ });
454
+ break;
455
+ }
456
+ }
457
+ // Create gate for constitution check
458
+ for (const section of allSections) {
459
+ if (section.heading.toLowerCase().includes("constitution")) {
460
+ const gateId = `${idPrefix}-GATE-CONST`;
461
+ nodes.push({
462
+ id: gateId,
463
+ type: "gate",
464
+ name: "Constitution Check",
465
+ description: section.body,
466
+ });
467
+ break;
468
+ }
469
+ }
470
+ // Create element for project structure
471
+ for (const section of allSections) {
472
+ if (section.heading.toLowerCase().includes("project structure")) {
473
+ const structId = `${idPrefix}-STRUCT`;
474
+ const lines = section.body.split("\n").filter((l) => l.trim());
475
+ nodes.push({
476
+ id: structId,
477
+ type: "element",
478
+ name: "Project Structure",
479
+ description: lines,
480
+ });
481
+ break;
482
+ }
483
+ }
484
+ // Add relationships
485
+ // plan depends_on spec
486
+ relationships.push({
487
+ from: planId,
488
+ to: `${idPrefix}-SPEC`,
489
+ type: "depends_on",
490
+ });
491
+ // gate governed_by protocol (if constitution exists)
492
+ const gateNode = nodes.find((n) => n.type === "gate" && n.id.includes("CONST"));
493
+ if (gateNode) {
494
+ relationships.push({
495
+ from: gateNode.id,
496
+ to: `${idPrefix}-CONST`,
497
+ type: "governed_by",
498
+ });
499
+ }
500
+ return { nodes, relationships };
501
+ }
502
+ // ---------------------------------------------------------------------------
503
+ // tasks.md parser
504
+ // ---------------------------------------------------------------------------
505
+ export function parseTasks(content, idPrefix) {
506
+ const sections = parseSections(content);
507
+ const allSections = flattenSections(sections);
508
+ const topLevelNodes = [];
509
+ const topLevelRelationships = [];
510
+ // Parse phases (## Phase N: Title)
511
+ const phases = [];
512
+ let phaseNum = 0;
513
+ for (const section of allSections) {
514
+ const phaseMatch = /^Phase\s+(\d+):\s*(.+)$/i.exec(section.heading);
515
+ if (phaseMatch) {
516
+ phaseNum++;
517
+ const phaseTitle = phaseMatch[2];
518
+ // Parse tasks in this phase
519
+ const tasks = parseCheckboxes(section.body);
520
+ phases.push({
521
+ title: phaseTitle,
522
+ phaseNum,
523
+ tasks,
524
+ });
525
+ }
526
+ }
527
+ // Build subsystem nodes and relationships
528
+ const subsystemNodes = [];
529
+ const subsystemRelationships = [];
530
+ // Group tasks by user story or phase
531
+ const changesByStory = {};
532
+ const changesByPhase = {};
533
+ for (const phase of phases) {
534
+ changesByPhase[phase.phaseNum] = [];
535
+ for (const task of phase.tasks) {
536
+ // Look for [US1], [US2], etc. in the task text
537
+ const storyMatch = /\[US(\d+)\]/.exec(task.text);
538
+ if (storyMatch) {
539
+ const storyKey = `US${storyMatch[1]}`;
540
+ (changesByStory[storyKey] ??= []).push(task);
541
+ }
542
+ else {
543
+ changesByPhase[phase.phaseNum].push(task);
544
+ }
545
+ }
546
+ }
547
+ // Create change nodes for each phase (with LOCAL IDs in subsystem)
548
+ // Use numeric indices (CHG-1, CHG-2, etc.) for phase changes
549
+ for (let i = 0; i < phases.length; i++) {
550
+ const phase = phases[i];
551
+ const tasks = changesByPhase[phase.phaseNum] ?? [];
552
+ const plan = tasks.map((t) => ({
553
+ description: t.text,
554
+ done: t.done,
555
+ }));
556
+ const changeLocalId = `CHG-${String(phase.phaseNum)}`;
557
+ subsystemNodes.push({
558
+ id: changeLocalId,
559
+ type: "change",
560
+ name: phase.title,
561
+ plan,
562
+ });
563
+ // Wire must_follow between consecutive phase changes
564
+ if (i > 0) {
565
+ const prevPhaseNum = phases[i - 1].phaseNum;
566
+ subsystemRelationships.push({
567
+ from: changeLocalId,
568
+ to: `CHG-${String(prevPhaseNum)}`,
569
+ type: "must_follow",
570
+ });
571
+ }
572
+ }
573
+ // Create change nodes for user stories (with LOCAL IDs in subsystem)
574
+ for (const [storyKey, tasks] of Object.entries(changesByStory)) {
575
+ const plan = tasks.map((t) => ({
576
+ description: t.text,
577
+ done: t.done,
578
+ }));
579
+ const changeLocalId = `CHG-${storyKey}`;
580
+ subsystemNodes.push({
581
+ id: changeLocalId,
582
+ type: "change",
583
+ name: storyKey,
584
+ plan,
585
+ });
586
+ // Link to the capability at the top level (using GLOBAL ID format)
587
+ const changeGlobalId = `${idPrefix}-CHG-${storyKey}`;
588
+ topLevelRelationships.push({
589
+ from: changeGlobalId,
590
+ to: `${idPrefix}-${storyKey}`,
591
+ type: "implements",
592
+ });
593
+ }
594
+ // Create implementation protocol with subsystem
595
+ const protocolId = `${idPrefix}-PROT-IMPL`;
596
+ topLevelNodes.push({
597
+ id: protocolId,
598
+ type: "protocol",
599
+ name: "Implementation Protocol",
600
+ subsystem: {
601
+ nodes: subsystemNodes,
602
+ relationships: subsystemRelationships,
603
+ },
604
+ });
605
+ return { nodes: topLevelNodes, relationships: topLevelRelationships };
606
+ }
607
+ // ---------------------------------------------------------------------------
608
+ // checklist.md parser
609
+ // ---------------------------------------------------------------------------
610
+ export function parseChecklist(content, idPrefix) {
611
+ const sections = parseSections(content);
612
+ const allSections = flattenSections(sections);
613
+ const nodes = [];
614
+ const relationships = [];
615
+ // Find title and extract checklist type
616
+ let title = "Checklist";
617
+ const titleSection = sections.find((s) => s.level === 1);
618
+ if (titleSection) {
619
+ title = titleSection.heading;
620
+ }
621
+ // Extract metadata (Purpose, Created, etc.)
622
+ const metadata = parseFrontMatterish(content);
623
+ const purpose = (metadata.Purpose || titleSection?.body) ?? "";
624
+ // Create gate node for the checklist
625
+ const gateId = `${idPrefix}-CHK`;
626
+ nodes.push({
627
+ id: gateId,
628
+ type: "gate",
629
+ name: title,
630
+ description: purpose,
631
+ context: metadata.Created,
632
+ });
633
+ // Parse all checkbox items and build lifecycle map
634
+ const lifecycle = {};
635
+ const categoryDescriptions = [];
636
+ for (const section of allSections) {
637
+ if (section.level === 2) {
638
+ categoryDescriptions.push(`### ${section.heading}`);
639
+ }
640
+ const items = parseCheckboxes(section.body);
641
+ for (const item of items) {
642
+ lifecycle[item.id] = item.done;
643
+ categoryDescriptions.push(`- [${item.done ? "x" : " "}] ${item.id} ${item.text}`);
644
+ }
645
+ }
646
+ // Add lifecycle and description to the gate node
647
+ if (nodes.length > 0) {
648
+ nodes[0].lifecycle = lifecycle;
649
+ if (categoryDescriptions.length > 0) {
650
+ nodes[0].description = categoryDescriptions;
651
+ }
652
+ }
653
+ return { nodes, relationships };
654
+ }
655
+ // ---------------------------------------------------------------------------
656
+ // Full feature directory parser
657
+ // ---------------------------------------------------------------------------
658
+ export function parseSpecKitFeature(featureDir, idPrefix, constitutionPath) {
659
+ const nodes = [];
660
+ const relationships = [];
661
+ // Helper to read and parse a file if it exists
662
+ const parseIfExists = (filePath, parser) => {
663
+ if (filePath && existsSync(filePath)) {
664
+ try {
665
+ const content = readFileSync(filePath, "utf-8");
666
+ const result = parser(content, idPrefix);
667
+ nodes.push(...result.nodes);
668
+ relationships.push(...result.relationships);
669
+ }
670
+ catch (error) {
671
+ // Log error but continue
672
+ console.warn(`Error parsing ${filePath}:`, error);
673
+ }
674
+ }
675
+ };
676
+ // Parse constitution if provided
677
+ if (constitutionPath && existsSync(constitutionPath)) {
678
+ try {
679
+ const content = readFileSync(constitutionPath, "utf-8");
680
+ const result = parseConstitution(content, idPrefix);
681
+ nodes.push(...result.nodes);
682
+ relationships.push(...result.relationships);
683
+ }
684
+ catch (error) {
685
+ console.warn(`Error parsing constitution:`, error);
686
+ }
687
+ }
688
+ // Parse spec.md
689
+ const specPath = join(featureDir, "spec.md");
690
+ parseIfExists(specPath, parseSpec);
691
+ // Parse plan.md
692
+ const planPath = join(featureDir, "plan.md");
693
+ parseIfExists(planPath, parsePlan);
694
+ // Parse tasks.md
695
+ const tasksPath = join(featureDir, "tasks.md");
696
+ parseIfExists(tasksPath, parseTasks);
697
+ // Parse checklist.md
698
+ const checklistPath = join(featureDir, "checklist.md");
699
+ parseIfExists(checklistPath, parseChecklist);
700
+ // Extract feature name from directory
701
+ const featureName = basename(featureDir);
702
+ // Build the SysProMDocument
703
+ const doc = {
704
+ metadata: {
705
+ title: featureName,
706
+ doc_type: "speckit",
707
+ },
708
+ nodes,
709
+ relationships: relationships.length > 0 ? relationships : undefined,
710
+ };
711
+ return doc;
712
+ }