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,498 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { NODE_FILE_MAP, NODE_TYPE_LABELS, NodeType, RelationshipType, RELATIONSHIP_TYPE_LABELS, } from "./schema.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Text helpers
6
+ // ---------------------------------------------------------------------------
7
+ function renderText(value) {
8
+ return Array.isArray(value) ? value.join("\n") : value;
9
+ }
10
+ function renderFrontMatter(fields) {
11
+ const lines = ["---"];
12
+ for (const [key, value] of Object.entries(fields)) {
13
+ if (value === undefined)
14
+ continue;
15
+ if (typeof value === "number") {
16
+ lines.push(`${key}: ${String(value)}`);
17
+ }
18
+ else {
19
+ lines.push(`${key}: ${JSON.stringify(value)}`);
20
+ }
21
+ }
22
+ lines.push("---");
23
+ return lines.join("\n");
24
+ }
25
+ function indexRelationshipsFrom(rels) {
26
+ const idx = new Map();
27
+ for (const r of rels) {
28
+ const list = idx.get(r.from);
29
+ if (list)
30
+ list.push(r);
31
+ else
32
+ idx.set(r.from, [r]);
33
+ }
34
+ return idx;
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // Node rendering
38
+ // ---------------------------------------------------------------------------
39
+ // Canonical lifecycle stage orderings from PROT1 (decision), PROT2 (change), PROT3 (node).
40
+ // Keys not in any ordering are appended at the end in their original order.
41
+ const LIFECYCLE_ORDER = [
42
+ "proposed",
43
+ "accepted",
44
+ "active",
45
+ "adopted",
46
+ "implemented",
47
+ "defined",
48
+ "introduced",
49
+ "in_progress",
50
+ "complete",
51
+ "consolidated",
52
+ "experimental",
53
+ "deprecated",
54
+ "retired",
55
+ "superseded",
56
+ "abandoned",
57
+ "deferred",
58
+ ];
59
+ function renderLifecycle(lifecycle) {
60
+ const entries = Object.entries(lifecycle);
61
+ entries.sort(([a], [b]) => {
62
+ const ai = LIFECYCLE_ORDER.indexOf(a);
63
+ const bi = LIFECYCLE_ORDER.indexOf(b);
64
+ // Unknown keys sort after known ones, preserving relative order
65
+ if (ai === -1 && bi === -1)
66
+ return 0;
67
+ if (ai === -1)
68
+ return 1;
69
+ if (bi === -1)
70
+ return -1;
71
+ return ai - bi;
72
+ });
73
+ return entries.map(([state, done]) => {
74
+ const checkbox = done ? "x" : " ";
75
+ const label = state.replace(/_/g, " ");
76
+ if (typeof done === "string") {
77
+ return `- [${checkbox}] ${label} (${done})`;
78
+ }
79
+ return `- [${checkbox}] ${label}`;
80
+ });
81
+ }
82
+ function renderNodeRelationships(nodeId, fromIdx) {
83
+ const rels = fromIdx.get(nodeId);
84
+ if (!rels || rels.length === 0)
85
+ return [];
86
+ const grouped = new Map();
87
+ for (const r of rels) {
88
+ const list = grouped.get(r.type);
89
+ if (list)
90
+ list.push(r.to);
91
+ else
92
+ grouped.set(r.type, [r.to]);
93
+ }
94
+ const lines = [];
95
+ for (const [type, targets] of grouped) {
96
+ const label = RelationshipType.is(type)
97
+ ? RELATIONSHIP_TYPE_LABELS[type]
98
+ : type;
99
+ if (targets.length === 1) {
100
+ lines.push(`- ${label}: ${targets[0]}`);
101
+ }
102
+ else {
103
+ lines.push(`- ${label}:`);
104
+ for (const t of targets) {
105
+ lines.push(` - ${t}`);
106
+ }
107
+ }
108
+ }
109
+ return lines;
110
+ }
111
+ function renderExternalReferences(refs) {
112
+ if (refs.length === 0)
113
+ return [];
114
+ const lines = ["", "#### External References", ""];
115
+ for (const ref of refs) {
116
+ lines.push(`- ${ref.role}: ${ref.identifier}`);
117
+ if (ref.description) {
118
+ lines.push(` - ${renderText(ref.description)}`);
119
+ }
120
+ if (ref.internalised) {
121
+ lines.push(` - Internalised: ${renderText(ref.internalised)}`);
122
+ }
123
+ }
124
+ return lines;
125
+ }
126
+ function renderNode(n, headingLevel, fromIdx) {
127
+ const prefix = "#".repeat(headingLevel);
128
+ const lines = [];
129
+ lines.push(`${prefix} ${n.id} — ${n.name}`);
130
+ lines.push("");
131
+ if (n.description) {
132
+ lines.push(renderText(n.description));
133
+ lines.push("");
134
+ }
135
+ const rels = renderNodeRelationships(n.id, fromIdx);
136
+ if (rels.length > 0) {
137
+ lines.push(...rels);
138
+ lines.push("");
139
+ }
140
+ if (n.status) {
141
+ lines.push(`- Status: ${n.status}`);
142
+ lines.push("");
143
+ }
144
+ // Decision fields
145
+ if (n.context) {
146
+ lines.push(`Context: ${renderText(n.context)}`);
147
+ lines.push("");
148
+ }
149
+ if (n.options && n.options.length > 0) {
150
+ lines.push("Options:");
151
+ for (const o of n.options) {
152
+ lines.push(`- ${o.id}: ${renderText(o.description)}`);
153
+ }
154
+ lines.push("");
155
+ }
156
+ if (n.selected) {
157
+ lines.push(`Chosen: ${n.selected}`);
158
+ lines.push("");
159
+ }
160
+ if (n.rationale) {
161
+ lines.push(`Rationale: ${renderText(n.rationale)}`);
162
+ lines.push("");
163
+ }
164
+ // Change fields
165
+ if (n.scope && n.scope.length > 0) {
166
+ lines.push("Scope:");
167
+ for (const s of n.scope) {
168
+ lines.push(`- ${s}`);
169
+ }
170
+ lines.push("");
171
+ }
172
+ if (n.operations && n.operations.length > 0) {
173
+ lines.push("Operations:");
174
+ for (const op of n.operations) {
175
+ const parts = [op.type];
176
+ if (op.target)
177
+ parts.push(op.target);
178
+ if (op.description)
179
+ parts.push(`— ${renderText(op.description)}`);
180
+ lines.push(`- ${parts.join(" ")}`);
181
+ }
182
+ lines.push("");
183
+ }
184
+ if (n.plan && n.plan.length > 0) {
185
+ lines.push(`${"#".repeat(headingLevel + 1)} Plan`);
186
+ lines.push("");
187
+ for (const t of n.plan) {
188
+ lines.push(`- [${t.done ? "x" : " "}] ${renderText(t.description)}`);
189
+ }
190
+ lines.push("");
191
+ }
192
+ // Lifecycle
193
+ if (n.lifecycle) {
194
+ lines.push(`${"#".repeat(headingLevel + 1)} Lifecycle`);
195
+ lines.push("");
196
+ lines.push(...renderLifecycle(n.lifecycle));
197
+ lines.push("");
198
+ }
199
+ // Propagation
200
+ if (n.propagation) {
201
+ lines.push(`${"#".repeat(headingLevel + 1)} Propagation`);
202
+ lines.push("");
203
+ lines.push(...renderLifecycle(n.propagation));
204
+ lines.push("");
205
+ }
206
+ // View includes
207
+ if (n.includes && n.includes.length > 0) {
208
+ lines.push("Includes:");
209
+ for (const inc of n.includes) {
210
+ lines.push(`- ${inc}`);
211
+ }
212
+ lines.push("");
213
+ }
214
+ // Artefact flow
215
+ if (n.input) {
216
+ lines.push(`- Input: ${n.input}`);
217
+ }
218
+ if (n.output) {
219
+ lines.push(`- Output: ${n.output}`);
220
+ }
221
+ if (n.input || n.output) {
222
+ lines.push("");
223
+ }
224
+ // Inline external references
225
+ if (n.external_references && n.external_references.length > 0) {
226
+ lines.push(...renderExternalReferences(n.external_references));
227
+ lines.push("");
228
+ }
229
+ // Subsystem note
230
+ if (n.subsystem) {
231
+ lines.push(`${"#".repeat(headingLevel + 1)} Subsystem`);
232
+ lines.push("");
233
+ const subNodes = n.subsystem.nodes;
234
+ const subRels = n.subsystem.relationships ?? [];
235
+ const subIdx = indexRelationshipsFrom(subRels);
236
+ for (const sub of subNodes) {
237
+ lines.push(...renderNode(sub, headingLevel + 2, subIdx));
238
+ }
239
+ }
240
+ return lines;
241
+ }
242
+ // ---------------------------------------------------------------------------
243
+ // File generators
244
+ // ---------------------------------------------------------------------------
245
+ function renderNodesGrouped(nodes, types, fromIdx, headingLevel) {
246
+ const lines = [];
247
+ for (const type of types) {
248
+ const matching = nodes.filter((n) => n.type === type);
249
+ if (matching.length === 0)
250
+ continue;
251
+ const label = NodeType.is(type) ? NODE_TYPE_LABELS[type] : type;
252
+ lines.push(`${"#".repeat(headingLevel)} ${label}`);
253
+ lines.push("");
254
+ for (const n of matching) {
255
+ lines.push(...renderNode(n, headingLevel + 1, fromIdx));
256
+ }
257
+ }
258
+ return lines;
259
+ }
260
+ function generateReadme(doc, fromIdx) {
261
+ const lines = [];
262
+ const title = doc.metadata?.title ?? "SysProM";
263
+ lines.push(renderFrontMatter({
264
+ title,
265
+ doc_type: doc.metadata?.doc_type ?? "sysprom",
266
+ scope: doc.metadata?.scope,
267
+ status: doc.metadata?.status,
268
+ version: doc.metadata?.version,
269
+ }));
270
+ lines.push("");
271
+ lines.push(`# ${title}`);
272
+ lines.push("");
273
+ // Intent description
274
+ const intent = doc.nodes.find((n) => n.type === "intent");
275
+ if (intent?.description) {
276
+ lines.push(renderText(intent.description));
277
+ lines.push("");
278
+ }
279
+ // Determine which files will exist based on present node types
280
+ const presentFiles = [];
281
+ const fileDescriptions = {
282
+ INTENT: {
283
+ label: "Understand why this exists",
284
+ role: "Enduring purpose, concepts, capabilities",
285
+ },
286
+ INVARIANTS: {
287
+ label: "Understand what must always hold",
288
+ role: "Rules that must hold across all valid states",
289
+ },
290
+ STATE: {
291
+ label: "Understand what currently exists",
292
+ role: "Current structure and active elements",
293
+ },
294
+ DECISIONS: {
295
+ label: "Understand why things are the way they are",
296
+ role: "Choices and rationale",
297
+ },
298
+ CHANGES: {
299
+ label: "Understand how it has evolved",
300
+ role: "Evolution over time",
301
+ },
302
+ };
303
+ for (const [fileName, types] of Object.entries(NODE_FILE_MAP)) {
304
+ if (doc.nodes.some((n) => types.includes(n.type))) {
305
+ const desc = fileDescriptions[fileName];
306
+ presentFiles.push({ file: fileName, ...desc });
307
+ }
308
+ }
309
+ // Navigation — only link to files that exist
310
+ if (presentFiles.length > 0) {
311
+ lines.push("## Navigation");
312
+ lines.push("");
313
+ for (const { file, label } of presentFiles) {
314
+ lines.push(`### ${label}`);
315
+ lines.push(`See: [${file}.md](./${file}.md)`);
316
+ lines.push("");
317
+ }
318
+ // Document roles table — only include present files
319
+ lines.push("## Document Roles");
320
+ lines.push("");
321
+ lines.push("| Document | Role |");
322
+ lines.push("|----------|------|");
323
+ for (const { file, role } of presentFiles) {
324
+ lines.push(`| ${file}.md | ${role} |`);
325
+ }
326
+ lines.push("");
327
+ }
328
+ // Views
329
+ const views = doc.nodes.filter((n) => n.type === "view");
330
+ if (views.length > 0) {
331
+ lines.push(...renderNodesGrouped(doc.nodes, ["view"], fromIdx, 2));
332
+ }
333
+ // Graph-level external references
334
+ if (doc.external_references && doc.external_references.length > 0) {
335
+ lines.push("## External References");
336
+ lines.push("");
337
+ for (const ref of doc.external_references) {
338
+ const parts = [`- ${ref.role}: ${ref.identifier}`];
339
+ if (ref.node_id)
340
+ parts.push(` - Node: ${ref.node_id}`);
341
+ if (ref.description)
342
+ parts.push(` - ${renderText(ref.description)}`);
343
+ lines.push(...parts);
344
+ }
345
+ lines.push("");
346
+ }
347
+ return lines.join("\n") + "\n";
348
+ }
349
+ function generateDocFile(doc, fileName, types, fromIdx) {
350
+ const lines = [];
351
+ lines.push(renderFrontMatter({
352
+ title: fileName.replace(".md", ""),
353
+ doc_type: fileName.replace(".md", "").toLowerCase(),
354
+ }));
355
+ lines.push("");
356
+ lines.push(`# ${fileName.replace(".md", "")}`);
357
+ lines.push("");
358
+ lines.push(...renderNodesGrouped(doc.nodes, types, fromIdx, 2));
359
+ return lines.join("\n") + "\n";
360
+ }
361
+ /**
362
+ * Convert a SysProM document to a single Markdown string.
363
+ *
364
+ * @param doc - The SysProM document to convert.
365
+ * @returns The Markdown representation.
366
+ */
367
+ export function jsonToMarkdownSingle(doc) {
368
+ const fromIdx = indexRelationshipsFrom(doc.relationships ?? []);
369
+ const lines = [];
370
+ const title = doc.metadata?.title ?? "SysProM";
371
+ lines.push(renderFrontMatter({
372
+ title,
373
+ doc_type: doc.metadata?.doc_type ?? "sysprom",
374
+ scope: doc.metadata?.scope,
375
+ status: doc.metadata?.status,
376
+ version: doc.metadata?.version,
377
+ }));
378
+ lines.push("");
379
+ lines.push(`# ${title}`);
380
+ lines.push("");
381
+ const allTypes = [
382
+ ...NODE_FILE_MAP.INTENT,
383
+ ...NODE_FILE_MAP.INVARIANTS,
384
+ ...NODE_FILE_MAP.STATE,
385
+ ...NODE_FILE_MAP.DECISIONS,
386
+ ...NODE_FILE_MAP.CHANGES,
387
+ "view",
388
+ "milestone",
389
+ "version",
390
+ ];
391
+ lines.push(...renderNodesGrouped(doc.nodes, allTypes, fromIdx, 2));
392
+ // Relationships summary
393
+ if (doc.relationships && doc.relationships.length > 0) {
394
+ lines.push("## Relationships");
395
+ lines.push("");
396
+ lines.push("| From | Type | To |");
397
+ lines.push("|------|------|----|");
398
+ for (const r of doc.relationships) {
399
+ lines.push(`| ${r.from} | ${r.type} | ${r.to} |`);
400
+ }
401
+ lines.push("");
402
+ }
403
+ // External references
404
+ if (doc.external_references && doc.external_references.length > 0) {
405
+ lines.push("## External References");
406
+ lines.push("");
407
+ for (const ref of doc.external_references) {
408
+ lines.push(`- ${ref.role}: ${ref.identifier}`);
409
+ if (ref.node_id)
410
+ lines.push(` - Node: ${ref.node_id}`);
411
+ if (ref.description)
412
+ lines.push(` - ${renderText(ref.description)}`);
413
+ }
414
+ lines.push("");
415
+ }
416
+ return lines.join("\n") + "\n";
417
+ }
418
+ /**
419
+ * Convert a SysProM document to a multi-document Markdown folder.
420
+ *
421
+ * @param doc - The SysProM document to convert.
422
+ * @param outDir - Output directory path.
423
+ */
424
+ export function jsonToMarkdownMultiDoc(doc, outDir) {
425
+ mkdirSync(outDir, { recursive: true });
426
+ const fromIdx = indexRelationshipsFrom(doc.relationships ?? []);
427
+ writeFileSync(join(outDir, "README.md"), generateReadme(doc, fromIdx));
428
+ for (const [fileName, types] of Object.entries(NODE_FILE_MAP)) {
429
+ const hasNodes = doc.nodes.some((n) => types.includes(n.type));
430
+ if (!hasNodes)
431
+ continue;
432
+ writeFileSync(join(outDir, `${fileName}.md`), generateDocFile(doc, fileName, types, fromIdx));
433
+ }
434
+ // Subsystem folders or single files
435
+ const subsystemNodes = doc.nodes.filter((n) => n.subsystem);
436
+ // Count subsystems per type to decide automatic grouping
437
+ const typeCounts = new Map();
438
+ for (const n of subsystemNodes) {
439
+ typeCounts.set(n.type, (typeCounts.get(n.type) ?? 0) + 1);
440
+ }
441
+ for (const n of subsystemNodes) {
442
+ const subsystem = n.subsystem;
443
+ if (!subsystem)
444
+ continue;
445
+ const subDoc = {
446
+ ...subsystem,
447
+ metadata: {
448
+ title: `${n.id} — ${n.name}`,
449
+ doc_type: n.type,
450
+ scope: n.type,
451
+ status: n.status,
452
+ },
453
+ };
454
+ // Count how many distinct file types would be produced
455
+ const fileCounts = Object.values(NODE_FILE_MAP).filter((types) => subDoc.nodes.some((sn) => types.includes(sn.type))).length;
456
+ const slug = `${n.id}-${n.name
457
+ .toLowerCase()
458
+ .replace(/[^a-z0-9]+/g, "-")
459
+ .replace(/-$/, "")}`;
460
+ // Auto-group when 2+ subsystems share the same type
461
+ let parentDir = outDir;
462
+ if ((typeCounts.get(n.type) ?? 0) >= 2 && NodeType.is(n.type)) {
463
+ const groupLabel = NODE_TYPE_LABELS[n.type]
464
+ .toLowerCase()
465
+ .replace(/ /g, "-");
466
+ parentDir = join(outDir, groupLabel);
467
+ mkdirSync(parentDir, { recursive: true });
468
+ }
469
+ if (fileCounts <= 1) {
470
+ const singleContent = jsonToMarkdownSingle(subDoc);
471
+ const lineCount = singleContent.split("\n").length;
472
+ if (lineCount <= 100) {
473
+ writeFileSync(join(parentDir, `${slug}.spm.md`), singleContent);
474
+ }
475
+ else {
476
+ jsonToMarkdownMultiDoc(subDoc, join(parentDir, slug));
477
+ }
478
+ }
479
+ else {
480
+ jsonToMarkdownMultiDoc(subDoc, join(parentDir, slug));
481
+ }
482
+ }
483
+ }
484
+ /**
485
+ * Convert a SysProM document to Markdown, writing to the specified output path.
486
+ *
487
+ * @param doc - The SysProM document to convert.
488
+ * @param output - Output file or directory path.
489
+ * @param options - Conversion options specifying single-file or multi-doc form.
490
+ */
491
+ export function jsonToMarkdown(doc, output, options) {
492
+ if (options.form === "single-file") {
493
+ writeFileSync(output, jsonToMarkdownSingle(doc));
494
+ }
495
+ else {
496
+ jsonToMarkdownMultiDoc(doc, output);
497
+ }
498
+ }
@@ -0,0 +1,22 @@
1
+ import { type SysProMDocument } from "./schema.js";
2
+ /**
3
+ * Parse a single Markdown file into a SysProM document.
4
+ *
5
+ * @param content - The Markdown content to parse.
6
+ * @returns The parsed SysProM document.
7
+ */
8
+ export declare function markdownSingleToJson(content: string): SysProMDocument;
9
+ /**
10
+ * Parse a multi-document Markdown folder into a SysProM document.
11
+ *
12
+ * @param dir - Path to the directory containing Markdown files.
13
+ * @returns The parsed SysProM document.
14
+ */
15
+ export declare function markdownMultiDocToJson(dir: string): SysProMDocument;
16
+ /**
17
+ * Parse Markdown into a SysProM document, auto-detecting single-file or multi-doc format.
18
+ *
19
+ * @param input - File path or directory path to parse.
20
+ * @returns The parsed SysProM document.
21
+ */
22
+ export declare function markdownToJson(input: string): SysProMDocument;