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,548 @@
1
+ import * as z from "zod";
2
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
3
+ import { join, basename } from "node:path";
4
+ import { NODE_FILE_MAP, NODE_LABEL_TO_TYPE, RELATIONSHIP_TYPE_LABELS, RELATIONSHIP_LABEL_TO_TYPE, NodeType, RelationshipType, NodeStatus, ExternalReferenceRole, } from "./schema.js";
5
+ const LABEL_TO_TYPE = Object.fromEntries(Object.entries(NODE_LABEL_TO_TYPE).map(([k, v]) => [k.toLowerCase(), v]));
6
+ const operationType = z.enum(["add", "update", "remove", "link"]);
7
+ function parseNodeType(s) {
8
+ const result = NodeType.safeParse(s);
9
+ if (!result.success)
10
+ throw new Error(`Unknown node type: ${s}`);
11
+ return result.data;
12
+ }
13
+ function parseRelType(s) {
14
+ const result = RelationshipType.safeParse(s);
15
+ if (!result.success)
16
+ throw new Error(`Unknown relationship type: ${s}`);
17
+ return result.data;
18
+ }
19
+ function parseNodeStatus(s) {
20
+ const result = NodeStatus.safeParse(s);
21
+ if (!result.success)
22
+ throw new Error(`Unknown node status: ${s}`);
23
+ return result.data;
24
+ }
25
+ function parseExtRefRole(s) {
26
+ const result = ExternalReferenceRole.safeParse(s);
27
+ if (!result.success)
28
+ throw new Error(`Unknown external reference role: ${s}`);
29
+ return result.data;
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Text helpers
33
+ // ---------------------------------------------------------------------------
34
+ function parseText(raw) {
35
+ const lines = raw.split("\n");
36
+ return lines.length === 1 ? lines[0] : lines;
37
+ }
38
+ function parseFrontMatter(content) {
39
+ if (!content.startsWith("---\n"))
40
+ return { front: {}, body: content };
41
+ const end = content.indexOf("\n---\n", 4);
42
+ if (end === -1)
43
+ return { front: {}, body: content };
44
+ const yaml = content.slice(4, end);
45
+ const front = {};
46
+ for (const line of yaml.split("\n")) {
47
+ const match = /^(\w+):\s*(.+)$/.exec(line);
48
+ if (!match)
49
+ continue;
50
+ const [, key, raw] = match;
51
+ if (raw.startsWith('"') && raw.endsWith('"')) {
52
+ front[key] = raw.slice(1, -1);
53
+ }
54
+ else if (/^\d+$/.test(raw)) {
55
+ front[key] = Number.parseInt(raw, 10);
56
+ }
57
+ else {
58
+ front[key] = raw;
59
+ }
60
+ }
61
+ return { front, body: content.slice(end + 5) };
62
+ }
63
+ function parseSections(body) {
64
+ const lines = body.split("\n");
65
+ const all = [];
66
+ // First pass: find all headings and their body text (until next heading of any level)
67
+ for (let i = 0; i < lines.length; i++) {
68
+ const hMatch = /^(#{1,6})\s+(.+)$/.exec(lines[i]);
69
+ if (hMatch) {
70
+ const level = hMatch[1].length;
71
+ const heading = hMatch[2];
72
+ const bodyLines = [];
73
+ for (let j = i + 1; j < lines.length; j++) {
74
+ if (/^#{1,6}\s/.exec(lines[j]))
75
+ break;
76
+ bodyLines.push(lines[j]);
77
+ }
78
+ all.push({
79
+ level,
80
+ heading,
81
+ body: bodyLines.join("\n").trim(),
82
+ children: [],
83
+ });
84
+ }
85
+ }
86
+ // Second pass: build tree
87
+ const root = [];
88
+ const stack = [];
89
+ for (const section of all) {
90
+ while (stack.length > 0 && stack[stack.length - 1].level >= section.level) {
91
+ stack.pop();
92
+ }
93
+ if (stack.length > 0) {
94
+ stack[stack.length - 1].children.push(section);
95
+ }
96
+ else {
97
+ root.push(section);
98
+ }
99
+ stack.push(section);
100
+ }
101
+ return root;
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Node parsing from sections
105
+ // ---------------------------------------------------------------------------
106
+ function parseNodeId(heading) {
107
+ const match = /^(\S+)\s+—\s+(.+)$/.exec(heading);
108
+ if (!match)
109
+ return null;
110
+ return { id: match[1], name: match[2] };
111
+ }
112
+ function parseLifecycle(section) {
113
+ const lifecycle = {};
114
+ let found = false;
115
+ for (const line of section.body.split("\n")) {
116
+ const m = /^- \[([ x])\] (.+)$/.exec(line);
117
+ if (m) {
118
+ const isChecked = m[1] === "x";
119
+ const text = m[2];
120
+ // Check if the text ends with a parenthesised date
121
+ const dateMatch = /(.+?)\s*\((\d{4}-\d{2}-\d{2}(?:T[^)]+)?)\)$/.exec(text);
122
+ const key = dateMatch
123
+ ? dateMatch[1].replace(/ /g, "_")
124
+ : text.replace(/ /g, "_");
125
+ // If a date is found, use the date string as the value regardless of checkbox state
126
+ if (dateMatch) {
127
+ lifecycle[key] = dateMatch[2];
128
+ }
129
+ else {
130
+ // Otherwise, use boolean value
131
+ lifecycle[key] = isChecked;
132
+ }
133
+ found = true;
134
+ }
135
+ }
136
+ return found ? lifecycle : undefined;
137
+ }
138
+ const RELATIONSHIP_LABELS = Object.values(RELATIONSHIP_TYPE_LABELS);
139
+ function isRelationshipLabel(line) {
140
+ return RELATIONSHIP_LABELS.some((label) => line.startsWith(`- ${label}:`));
141
+ }
142
+ function parseListItems(body, prefix) {
143
+ const items = [];
144
+ let collecting = false;
145
+ for (const line of body.split("\n")) {
146
+ if (line.startsWith(`${prefix}:`)) {
147
+ collecting = true;
148
+ const inline = line.slice(prefix.length + 1).trim();
149
+ if (inline) {
150
+ items.push(inline);
151
+ collecting = false;
152
+ }
153
+ continue;
154
+ }
155
+ if (collecting && line.startsWith(" - ")) {
156
+ items.push(line.slice(4));
157
+ }
158
+ else if (collecting &&
159
+ line.startsWith("- ") &&
160
+ !isRelationshipLabel(line)) {
161
+ items.push(line.slice(2));
162
+ }
163
+ else if (collecting) {
164
+ collecting = false;
165
+ }
166
+ }
167
+ return items;
168
+ }
169
+ function parseSingleValue(body, prefix) {
170
+ for (const line of body.split("\n")) {
171
+ if (line.startsWith(`${prefix}: `)) {
172
+ return line.slice(prefix.length + 2);
173
+ }
174
+ }
175
+ return undefined;
176
+ }
177
+ function parseRelationshipsFromBody(body, nodeId) {
178
+ const rels = [];
179
+ for (const [label, type] of Object.entries(RELATIONSHIP_LABEL_TO_TYPE)) {
180
+ const relType = parseRelType(type);
181
+ const items = parseListItems(body, `- ${label}`);
182
+ if (items.length === 0) {
183
+ const val = parseSingleValue(body, `- ${label}`);
184
+ if (val) {
185
+ rels.push({ from: nodeId, to: val, type: relType });
186
+ }
187
+ }
188
+ else {
189
+ for (const target of items) {
190
+ rels.push({ from: nodeId, to: target, type: relType });
191
+ }
192
+ }
193
+ }
194
+ return rels;
195
+ }
196
+ function parseNodeFromSection(section) {
197
+ const parsed = parseNodeId(section.heading);
198
+ if (!parsed)
199
+ return null;
200
+ const { id, name } = parsed;
201
+ const body = section.body;
202
+ const node = { id, type: parseNodeType("intent"), name }; // type overwritten by caller
203
+ // Description is the first paragraph(s) before any list or sub-heading content
204
+ const descLines = [];
205
+ for (const line of body.split("\n")) {
206
+ if (line.startsWith("- ") ||
207
+ line.startsWith("Context:") ||
208
+ line.startsWith("Options:") ||
209
+ line.startsWith("Chosen:") ||
210
+ line.startsWith("Rationale:") ||
211
+ line.startsWith("Scope:") ||
212
+ line.startsWith("Operations:") ||
213
+ line.startsWith("Includes:") ||
214
+ line === "") {
215
+ if (descLines.length > 0)
216
+ break;
217
+ if (line === "")
218
+ continue;
219
+ break;
220
+ }
221
+ descLines.push(line);
222
+ }
223
+ if (descLines.length > 0) {
224
+ node.description = parseText(descLines.join("\n"));
225
+ }
226
+ // Status
227
+ const status = parseSingleValue(body, "- Status");
228
+ if (status)
229
+ node.status = parseNodeStatus(status);
230
+ // Decision fields
231
+ const context = parseSingleValue(body, "Context");
232
+ if (context)
233
+ node.context = parseText(context);
234
+ const chosen = parseSingleValue(body, "Chosen");
235
+ if (chosen)
236
+ node.selected = chosen;
237
+ const rationale = parseSingleValue(body, "Rationale");
238
+ if (rationale)
239
+ node.rationale = parseText(rationale);
240
+ // Options
241
+ const optionLines = parseListItems(body, "Options");
242
+ if (optionLines.length > 0) {
243
+ node.options = optionLines.map((line) => {
244
+ const m = /^(\S+):\s+(.+)$/.exec(line);
245
+ return m
246
+ ? { id: m[1], description: m[2] }
247
+ : { id: line, description: line };
248
+ });
249
+ }
250
+ // Change fields
251
+ const scopeItems = parseListItems(body, "Scope");
252
+ if (scopeItems.length > 0)
253
+ node.scope = scopeItems;
254
+ const opLines = parseListItems(body, "Operations");
255
+ if (opLines.length > 0) {
256
+ node.operations = opLines.map((line) => {
257
+ const parts = line.split(" ");
258
+ const rawType = parts[0];
259
+ const parsed = operationType.safeParse(rawType);
260
+ if (!parsed.success) {
261
+ throw new Error(`Unknown operation type: ${rawType}`);
262
+ }
263
+ const type = parsed.data;
264
+ const rest = parts.slice(1);
265
+ const dashIdx = rest.indexOf("—");
266
+ if (dashIdx >= 0) {
267
+ return {
268
+ type,
269
+ target: rest.slice(0, dashIdx).join(" ") || undefined,
270
+ description: rest.slice(dashIdx + 1).join(" "),
271
+ };
272
+ }
273
+ return { type, target: rest.join(" ") || undefined };
274
+ });
275
+ }
276
+ // View includes
277
+ const includes = parseListItems(body, "Includes");
278
+ if (includes.length > 0)
279
+ node.includes = includes;
280
+ // Artefact flow
281
+ const input = parseSingleValue(body, "- Input");
282
+ if (input)
283
+ node.input = input;
284
+ const output = parseSingleValue(body, "- Output");
285
+ if (output)
286
+ node.output = output;
287
+ // Lifecycle and propagation from child sections
288
+ for (const child of section.children) {
289
+ if (child.heading === "Lifecycle") {
290
+ node.lifecycle = parseLifecycle(child);
291
+ }
292
+ if (child.heading === "Propagation") {
293
+ const parsed = parseLifecycle(child);
294
+ // Propagation values are always boolean — coerce any date strings to true.
295
+ if (parsed) {
296
+ const booleanOnly = {};
297
+ for (const [k, v] of Object.entries(parsed)) {
298
+ booleanOnly[k] = !!v;
299
+ }
300
+ node.propagation = booleanOnly;
301
+ }
302
+ }
303
+ if (child.heading === "Plan") {
304
+ const plan = [];
305
+ for (const line of child.body.split("\n")) {
306
+ const m = /^- \[([ x])\] (.+)$/.exec(line);
307
+ if (m)
308
+ plan.push({ description: m[2], done: m[1] === "x" });
309
+ }
310
+ if (plan.length > 0)
311
+ node.plan = plan;
312
+ }
313
+ }
314
+ // Relationships
315
+ const rels = parseRelationshipsFromBody(body, id);
316
+ return { node, rels };
317
+ }
318
+ // ---------------------------------------------------------------------------
319
+ // File-level parsing
320
+ // ---------------------------------------------------------------------------
321
+ function findTypeSections(sections) {
322
+ // Type sections (## Intent, ## Concepts, etc.) may be at root level
323
+ // or nested under a top-level # heading. Flatten to find them.
324
+ const result = [];
325
+ for (const s of sections) {
326
+ if (LABEL_TO_TYPE[s.heading.toLowerCase()]) {
327
+ result.push(s);
328
+ }
329
+ for (const child of s.children) {
330
+ if (LABEL_TO_TYPE[child.heading.toLowerCase()]) {
331
+ result.push(child);
332
+ }
333
+ }
334
+ }
335
+ return result;
336
+ }
337
+ function parseDocFile(content, types) {
338
+ const { body } = parseFrontMatter(content);
339
+ const sections = parseSections(body);
340
+ const typeSections = findTypeSections(sections);
341
+ const nodes = [];
342
+ const rels = [];
343
+ for (const typeSection of typeSections) {
344
+ const type = LABEL_TO_TYPE[typeSection.heading.toLowerCase()] ??
345
+ types.find((t) => typeSection.heading.toLowerCase() === t);
346
+ for (const child of typeSection.children) {
347
+ const result = parseNodeFromSection(child);
348
+ if (result) {
349
+ result.node.type = parseNodeType(type);
350
+ nodes.push(result.node);
351
+ rels.push(...result.rels);
352
+ }
353
+ }
354
+ }
355
+ return { nodes, rels };
356
+ }
357
+ // ---------------------------------------------------------------------------
358
+ // External references from README
359
+ // ---------------------------------------------------------------------------
360
+ function parseExternalReferences(body) {
361
+ const refs = [];
362
+ const lines = body.split("\n");
363
+ let inSection = false;
364
+ for (let i = 0; i < lines.length; i++) {
365
+ if (/^##\s+External References/.exec(lines[i])) {
366
+ inSection = true;
367
+ continue;
368
+ }
369
+ if (inSection && /^##\s/.exec(lines[i]))
370
+ break;
371
+ if (inSection && lines[i].startsWith("- ")) {
372
+ const m = /^- (\w+): (.+)$/.exec(lines[i]);
373
+ if (m) {
374
+ const ref = {
375
+ role: parseExtRefRole(m[1]),
376
+ identifier: m[2],
377
+ };
378
+ // Check for indented sub-items
379
+ for (let j = i + 1; j < lines.length && lines[j].startsWith(" - "); j++) {
380
+ const sub = lines[j].slice(4);
381
+ if (sub.startsWith("Node: "))
382
+ ref.node_id = sub.slice(6);
383
+ else
384
+ ref.description = sub;
385
+ i = j;
386
+ }
387
+ refs.push(ref);
388
+ }
389
+ }
390
+ }
391
+ return refs;
392
+ }
393
+ // ---------------------------------------------------------------------------
394
+ // Relationship table from single file
395
+ // ---------------------------------------------------------------------------
396
+ function parseRelationshipTable(body) {
397
+ const rels = [];
398
+ const lines = body.split("\n");
399
+ let inTable = false;
400
+ for (const line of lines) {
401
+ if (line.startsWith("| From |")) {
402
+ inTable = true;
403
+ continue;
404
+ }
405
+ if (inTable && line.startsWith("|---"))
406
+ continue;
407
+ if (inTable && line.startsWith("|")) {
408
+ const cells = line
409
+ .split("|")
410
+ .map((c) => c.trim())
411
+ .filter(Boolean);
412
+ if (cells.length >= 3) {
413
+ rels.push({
414
+ from: cells[0],
415
+ to: cells[2],
416
+ type: parseRelType(cells[1]),
417
+ });
418
+ }
419
+ }
420
+ else if (inTable) {
421
+ inTable = false;
422
+ }
423
+ }
424
+ return rels;
425
+ }
426
+ // ---------------------------------------------------------------------------
427
+ // Public API
428
+ // ---------------------------------------------------------------------------
429
+ /**
430
+ * Parse a single Markdown file into a SysProM document.
431
+ *
432
+ * @param content - The Markdown content to parse.
433
+ * @returns The parsed SysProM document.
434
+ */
435
+ export function markdownSingleToJson(content) {
436
+ const { front, body } = parseFrontMatter(content);
437
+ const allTypes = [
438
+ ...NODE_FILE_MAP.INTENT,
439
+ ...NODE_FILE_MAP.INVARIANTS,
440
+ ...NODE_FILE_MAP.STATE,
441
+ ...NODE_FILE_MAP.DECISIONS,
442
+ ...NODE_FILE_MAP.CHANGES,
443
+ "view",
444
+ "milestone",
445
+ "version",
446
+ ];
447
+ const { nodes, rels } = parseDocFile(content, allTypes);
448
+ const tableRels = parseRelationshipTable(body);
449
+ const extRefs = parseExternalReferences(body);
450
+ const doc = {
451
+ metadata: Object.keys(front).length > 0 ? front : undefined,
452
+ nodes,
453
+ relationships: [...rels, ...tableRels].length > 0 ? [...rels, ...tableRels] : undefined,
454
+ external_references: extRefs.length > 0 ? extRefs : undefined,
455
+ };
456
+ if (front.title && typeof front.title === "string") {
457
+ doc.metadata = { ...front };
458
+ }
459
+ return doc;
460
+ }
461
+ /**
462
+ * Parse a multi-document Markdown folder into a SysProM document.
463
+ *
464
+ * @param dir - Path to the directory containing Markdown files.
465
+ * @returns The parsed SysProM document.
466
+ */
467
+ export function markdownMultiDocToJson(dir) {
468
+ const readmeContent = readFileSync(join(dir, "README.md"), "utf8");
469
+ const { front, body } = parseFrontMatter(readmeContent);
470
+ const nodes = [];
471
+ const rels = [];
472
+ // Parse each document file
473
+ for (const [fileName, types] of Object.entries(NODE_FILE_MAP)) {
474
+ const filePath = join(dir, `${fileName}.md`);
475
+ if (!existsSync(filePath))
476
+ continue;
477
+ const content = readFileSync(filePath, "utf8");
478
+ const result = parseDocFile(content, types);
479
+ nodes.push(...result.nodes);
480
+ rels.push(...result.rels);
481
+ }
482
+ // Parse views, milestones, versions from README
483
+ const readmeSections = parseSections(body);
484
+ const readmeTypeSections = findTypeSections(readmeSections);
485
+ for (const typeSection of readmeTypeSections) {
486
+ const type = LABEL_TO_TYPE[typeSection.heading.toLowerCase()];
487
+ if (!type)
488
+ continue;
489
+ for (const child of typeSection.children) {
490
+ const result = parseNodeFromSection(child);
491
+ if (result) {
492
+ result.node.type = parseNodeType(type);
493
+ nodes.push(result.node);
494
+ rels.push(...result.rels);
495
+ }
496
+ }
497
+ }
498
+ // External references from README
499
+ const extRefs = parseExternalReferences(body);
500
+ // Subsystem folders and .spm.md files (including inside grouping directories)
501
+ function scanForSubsystems(scanDir) {
502
+ for (const entry of readdirSync(scanDir)) {
503
+ const entryPath = join(scanDir, entry);
504
+ if (statSync(entryPath).isDirectory() &&
505
+ existsSync(join(entryPath, "README.md"))) {
506
+ // Folder-based subsystem
507
+ const idPrefix = entry.split("-")[0];
508
+ const parentNode = nodes.find((n) => n.id === idPrefix);
509
+ if (parentNode) {
510
+ parentNode.subsystem = markdownMultiDocToJson(entryPath);
511
+ }
512
+ }
513
+ else if (entry.endsWith(".spm.md")) {
514
+ // Single-file subsystem
515
+ const fileIdPrefix = basename(entry, ".spm.md").split("-")[0];
516
+ const parentNode = nodes.find((n) => n.id === fileIdPrefix);
517
+ if (parentNode) {
518
+ parentNode.subsystem = markdownSingleToJson(readFileSync(entryPath, "utf8"));
519
+ }
520
+ }
521
+ else if (statSync(entryPath).isDirectory() &&
522
+ !existsSync(join(entryPath, "README.md"))) {
523
+ // Grouping directory (no README = not a subsystem, just organisational)
524
+ scanForSubsystems(entryPath);
525
+ }
526
+ }
527
+ }
528
+ scanForSubsystems(dir);
529
+ const doc = {
530
+ metadata: Object.keys(front).length > 0 ? front : undefined,
531
+ nodes,
532
+ relationships: rels.length > 0 ? rels : undefined,
533
+ external_references: extRefs.length > 0 ? extRefs : undefined,
534
+ };
535
+ return doc;
536
+ }
537
+ /**
538
+ * Parse Markdown into a SysProM document, auto-detecting single-file or multi-doc format.
539
+ *
540
+ * @param input - File path or directory path to parse.
541
+ * @returns The parsed SysProM document.
542
+ */
543
+ export function markdownToJson(input) {
544
+ if (statSync(input).isDirectory()) {
545
+ return markdownMultiDocToJson(input);
546
+ }
547
+ return markdownSingleToJson(readFileSync(input, "utf8"));
548
+ }