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.
- package/README.md +207 -0
- package/dist/schema.json +510 -0
- package/dist/src/canonical-json.d.ts +23 -0
- package/dist/src/canonical-json.js +120 -0
- package/dist/src/cli/commands/add.d.ts +22 -0
- package/dist/src/cli/commands/add.js +95 -0
- package/dist/src/cli/commands/check.d.ts +10 -0
- package/dist/src/cli/commands/check.js +33 -0
- package/dist/src/cli/commands/graph.d.ts +15 -0
- package/dist/src/cli/commands/graph.js +32 -0
- package/dist/src/cli/commands/init.d.ts +2 -0
- package/dist/src/cli/commands/init.js +44 -0
- package/dist/src/cli/commands/json2md.d.ts +2 -0
- package/dist/src/cli/commands/json2md.js +60 -0
- package/dist/src/cli/commands/md2json.d.ts +2 -0
- package/dist/src/cli/commands/md2json.js +29 -0
- package/dist/src/cli/commands/plan.d.ts +2 -0
- package/dist/src/cli/commands/plan.js +227 -0
- package/dist/src/cli/commands/query.d.ts +2 -0
- package/dist/src/cli/commands/query.js +275 -0
- package/dist/src/cli/commands/remove.d.ts +13 -0
- package/dist/src/cli/commands/remove.js +50 -0
- package/dist/src/cli/commands/rename.d.ts +14 -0
- package/dist/src/cli/commands/rename.js +34 -0
- package/dist/src/cli/commands/search.d.ts +11 -0
- package/dist/src/cli/commands/search.js +37 -0
- package/dist/src/cli/commands/speckit.d.ts +2 -0
- package/dist/src/cli/commands/speckit.js +318 -0
- package/dist/src/cli/commands/stats.d.ts +10 -0
- package/dist/src/cli/commands/stats.js +51 -0
- package/dist/src/cli/commands/task.d.ts +2 -0
- package/dist/src/cli/commands/task.js +162 -0
- package/dist/src/cli/commands/update.d.ts +2 -0
- package/dist/src/cli/commands/update.js +219 -0
- package/dist/src/cli/commands/validate.d.ts +10 -0
- package/dist/src/cli/commands/validate.js +30 -0
- package/dist/src/cli/define-command.d.ts +34 -0
- package/dist/src/cli/define-command.js +237 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +3 -0
- package/dist/src/cli/program.d.ts +4 -0
- package/dist/src/cli/program.js +46 -0
- package/dist/src/cli/shared.d.ts +26 -0
- package/dist/src/cli/shared.js +41 -0
- package/dist/src/generate-schema.d.ts +1 -0
- package/dist/src/generate-schema.js +9 -0
- package/dist/src/index.d.ts +48 -0
- package/dist/src/index.js +99 -0
- package/dist/src/io.d.ts +22 -0
- package/dist/src/io.js +66 -0
- package/dist/src/json-to-md.d.ts +26 -0
- package/dist/src/json-to-md.js +498 -0
- package/dist/src/md-to-json.d.ts +22 -0
- package/dist/src/md-to-json.js +548 -0
- package/dist/src/operations/add-node.d.ts +887 -0
- package/dist/src/operations/add-node.js +21 -0
- package/dist/src/operations/add-plan-task.d.ts +594 -0
- package/dist/src/operations/add-plan-task.js +25 -0
- package/dist/src/operations/add-relationship.d.ts +635 -0
- package/dist/src/operations/add-relationship.js +25 -0
- package/dist/src/operations/check.d.ts +301 -0
- package/dist/src/operations/check.js +66 -0
- package/dist/src/operations/define-operation.d.ts +14 -0
- package/dist/src/operations/define-operation.js +21 -0
- package/dist/src/operations/graph.d.ts +303 -0
- package/dist/src/operations/graph.js +71 -0
- package/dist/src/operations/index.d.ts +38 -0
- package/dist/src/operations/index.js +45 -0
- package/dist/src/operations/init-document.d.ts +299 -0
- package/dist/src/operations/init-document.js +26 -0
- package/dist/src/operations/json-to-markdown.d.ts +298 -0
- package/dist/src/operations/json-to-markdown.js +13 -0
- package/dist/src/operations/mark-task-done.d.ts +594 -0
- package/dist/src/operations/mark-task-done.js +26 -0
- package/dist/src/operations/mark-task-undone.d.ts +594 -0
- package/dist/src/operations/mark-task-undone.js +26 -0
- package/dist/src/operations/markdown-to-json.d.ts +298 -0
- package/dist/src/operations/markdown-to-json.js +13 -0
- package/dist/src/operations/next-id.d.ts +322 -0
- package/dist/src/operations/next-id.js +29 -0
- package/dist/src/operations/node-history.d.ts +313 -0
- package/dist/src/operations/node-history.js +55 -0
- package/dist/src/operations/plan-add-task.d.ts +595 -0
- package/dist/src/operations/plan-add-task.js +18 -0
- package/dist/src/operations/plan-gate.d.ts +351 -0
- package/dist/src/operations/plan-gate.js +41 -0
- package/dist/src/operations/plan-init.d.ts +299 -0
- package/dist/src/operations/plan-init.js +17 -0
- package/dist/src/operations/plan-progress.d.ts +313 -0
- package/dist/src/operations/plan-progress.js +23 -0
- package/dist/src/operations/plan-status.d.ts +349 -0
- package/dist/src/operations/plan-status.js +41 -0
- package/dist/src/operations/query-node.d.ts +1065 -0
- package/dist/src/operations/query-node.js +27 -0
- package/dist/src/operations/query-nodes.d.ts +594 -0
- package/dist/src/operations/query-nodes.js +23 -0
- package/dist/src/operations/query-relationships.d.ts +343 -0
- package/dist/src/operations/query-relationships.js +27 -0
- package/dist/src/operations/remove-node.d.ts +895 -0
- package/dist/src/operations/remove-node.js +58 -0
- package/dist/src/operations/remove-relationship.d.ts +622 -0
- package/dist/src/operations/remove-relationship.js +26 -0
- package/dist/src/operations/rename.d.ts +594 -0
- package/dist/src/operations/rename.js +113 -0
- package/dist/src/operations/search.d.ts +593 -0
- package/dist/src/operations/search.js +39 -0
- package/dist/src/operations/speckit-diff.d.ts +330 -0
- package/dist/src/operations/speckit-diff.js +89 -0
- package/dist/src/operations/speckit-export.d.ts +300 -0
- package/dist/src/operations/speckit-export.js +17 -0
- package/dist/src/operations/speckit-import.d.ts +299 -0
- package/dist/src/operations/speckit-import.js +39 -0
- package/dist/src/operations/speckit-sync.d.ts +900 -0
- package/dist/src/operations/speckit-sync.js +116 -0
- package/dist/src/operations/state-at.d.ts +309 -0
- package/dist/src/operations/state-at.js +53 -0
- package/dist/src/operations/stats.d.ts +324 -0
- package/dist/src/operations/stats.js +85 -0
- package/dist/src/operations/task-list.d.ts +305 -0
- package/dist/src/operations/task-list.js +44 -0
- package/dist/src/operations/timeline.d.ts +312 -0
- package/dist/src/operations/timeline.js +46 -0
- package/dist/src/operations/trace-from-node.d.ts +1197 -0
- package/dist/src/operations/trace-from-node.js +36 -0
- package/dist/src/operations/update-metadata.d.ts +593 -0
- package/dist/src/operations/update-metadata.js +18 -0
- package/dist/src/operations/update-node.d.ts +957 -0
- package/dist/src/operations/update-node.js +24 -0
- package/dist/src/operations/update-plan-task.d.ts +595 -0
- package/dist/src/operations/update-plan-task.js +31 -0
- package/dist/src/operations/validate.d.ts +310 -0
- package/dist/src/operations/validate.js +82 -0
- package/dist/src/schema.d.ts +891 -0
- package/dist/src/schema.js +356 -0
- package/dist/src/speckit/generate.d.ts +7 -0
- package/dist/src/speckit/generate.js +546 -0
- package/dist/src/speckit/index.d.ts +4 -0
- package/dist/src/speckit/index.js +4 -0
- package/dist/src/speckit/parse.d.ts +11 -0
- package/dist/src/speckit/parse.js +712 -0
- package/dist/src/speckit/plan.d.ts +125 -0
- package/dist/src/speckit/plan.js +636 -0
- package/dist/src/speckit/project.d.ts +39 -0
- package/dist/src/speckit/project.js +141 -0
- package/dist/src/text.d.ts +23 -0
- package/dist/src/text.js +32 -0
- package/package.json +86 -8
- 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
|
+
}
|