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,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;
|