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,546 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { textToString } from "../text.js";
4
+ // ============================================================================
5
+ // Helper functions
6
+ // ============================================================================
7
+ /**
8
+ * Find a single node by ID, or null if not found.
9
+ */
10
+ function findNode(doc, id) {
11
+ return doc.nodes.find((n) => n.id === id) ?? null;
12
+ }
13
+ /**
14
+ * Find all nodes of a specific type.
15
+ */
16
+ function findNodesByType(doc, type) {
17
+ return doc.nodes.filter((n) => n.type === type);
18
+ }
19
+ /**
20
+ * Find relationships from a source node to nodes of a target type.
21
+ */
22
+ function findRelationshipsFrom(doc, fromId, relationType) {
23
+ return (doc.relationships ?? []).filter((r) => {
24
+ if (r.from !== fromId)
25
+ return false;
26
+ if (relationType && r.type !== relationType)
27
+ return false;
28
+ return true;
29
+ });
30
+ }
31
+ /**
32
+ * Find relationships to a target node.
33
+ */
34
+ function findRelationshipsTo(doc, toId, relationType) {
35
+ return (doc.relationships ?? []).filter((r) => {
36
+ if (r.to !== toId)
37
+ return false;
38
+ if (relationType && r.type !== relationType)
39
+ return false;
40
+ return true;
41
+ });
42
+ }
43
+ /**
44
+ * Extract priority from a node's name, description, or lifecycle fields.
45
+ * Looks for patterns like "P1", "P2", "Priority: P1", etc.
46
+ */
47
+ function extractPriority(node) {
48
+ const text = [
49
+ node.name,
50
+ node.description ? textToString(node.description) : "",
51
+ Object.keys(node.lifecycle ?? {}).join(" "),
52
+ ]
53
+ .join(" ")
54
+ .toUpperCase();
55
+ const match = /P[1-5]/.exec(text);
56
+ return match ? match[0] : "P3";
57
+ }
58
+ /**
59
+ * Extract numeric suffix from an ID (e.g., "PREFIX-SPEC-001" -> "001").
60
+ */
61
+ function getIdSuffix(id) {
62
+ const parts = id.split("-");
63
+ return parts[parts.length - 1] ?? "000";
64
+ }
65
+ /**
66
+ * Parse tasks from a change node's plan array.
67
+ */
68
+ function parseTasks(node) {
69
+ return (node.plan ?? []).map((task) => ({
70
+ description: textToString(task.description),
71
+ done: task.done ?? false,
72
+ }));
73
+ }
74
+ /**
75
+ * Format the status for spec output: "proposed" -> "Draft", etc.
76
+ */
77
+ function formatStatus(status) {
78
+ if (!status)
79
+ return "Draft";
80
+ const mapping = {
81
+ proposed: "Draft",
82
+ active: "Active",
83
+ complete: "Complete",
84
+ };
85
+ return mapping[status] ?? status;
86
+ }
87
+ // ============================================================================
88
+ // generate Constitution
89
+ // ============================================================================
90
+ export function generateConstitution(doc, prefix) {
91
+ const protocolId = `${prefix}-CONST`;
92
+ const protocol = findNode(doc, protocolId);
93
+ if (!protocol) {
94
+ return "";
95
+ }
96
+ const title = protocol.name;
97
+ let output = `# ${title} Constitution\n\n`;
98
+ // Core Principles section
99
+ output += "## Core Principles\n\n";
100
+ const invariantRels = findRelationshipsTo(doc, protocolId, "part_of");
101
+ const principleIds = invariantRels
102
+ .map((r) => r.from)
103
+ .map((id) => findNode(doc, id))
104
+ .filter((n) => n !== null);
105
+ if (principleIds.length === 0) {
106
+ output += "*(No principles defined)*\n\n";
107
+ }
108
+ else {
109
+ for (const principle of principleIds) {
110
+ output += `### ${principle.name}\n\n`;
111
+ if (principle.description) {
112
+ output += `${textToString(principle.description)}\n\n`;
113
+ }
114
+ }
115
+ }
116
+ // Governance section
117
+ const govPolicyId = `${prefix}-POL-GOV`;
118
+ const govPolicy = findNode(doc, govPolicyId);
119
+ if (govPolicy) {
120
+ output += "## Governance\n\n";
121
+ if (govPolicy.description) {
122
+ output += `${textToString(govPolicy.description)}\n\n`;
123
+ }
124
+ }
125
+ // Other policy sections
126
+ const policyRels = findRelationshipsTo(doc, protocolId, "part_of");
127
+ const policyIds = policyRels
128
+ .map((r) => r.from)
129
+ .map((id) => findNode(doc, id))
130
+ .filter((n) => n !== null && n.type === "policy" && n.id !== govPolicyId);
131
+ for (const policy of policyIds) {
132
+ if (!policy)
133
+ continue;
134
+ output += `## ${policy.name}\n\n`;
135
+ if (policy.description) {
136
+ output += `${textToString(policy.description)}\n\n`;
137
+ }
138
+ }
139
+ // Footer with metadata
140
+ const version = protocol.lifecycle?.version ? "1.0" : "";
141
+ const ratified = protocol.lifecycle?.ratified
142
+ ? new Date().toISOString().split("T")[0]
143
+ : "";
144
+ const amended = protocol.lifecycle?.amended
145
+ ? new Date().toISOString().split("T")[0]
146
+ : "";
147
+ const footer = [
148
+ version ? `**Version**: ${version}` : undefined,
149
+ ratified ? `**Ratified**: ${ratified}` : undefined,
150
+ amended ? `**Last Amended**: ${amended}` : undefined,
151
+ ]
152
+ .filter(Boolean)
153
+ .join(" | ");
154
+ if (footer) {
155
+ output += footer + "\n";
156
+ }
157
+ return output.trim();
158
+ }
159
+ // ============================================================================
160
+ // generateSpec
161
+ // ============================================================================
162
+ export function generateSpec(doc, prefix) {
163
+ const specId = `${prefix}-SPEC`;
164
+ const spec = findNode(doc, specId);
165
+ if (!spec) {
166
+ return "";
167
+ }
168
+ const title = spec.name;
169
+ let output = `# Feature Specification: ${title}\n\n`;
170
+ // Metadata
171
+ output += `**Feature Branch**: \`${prefix.toLowerCase()}\`\n`;
172
+ output += `**Created**: ${new Date().toISOString().split("T")[0]}\n`;
173
+ output += `**Status**: ${formatStatus(spec.status)}\n\n`;
174
+ // User Scenarios & Testing section
175
+ output += "## User Scenarios & Testing *(mandatory)*\n\n";
176
+ const capabilityRels = findRelationshipsTo(doc, specId, "refines");
177
+ const capabilityIds = capabilityRels
178
+ .map((r) => r.from)
179
+ .map((id) => findNode(doc, id));
180
+ // Sort by ID suffix number
181
+ capabilityIds.sort((a, b) => {
182
+ const suffixA = parseInt(getIdSuffix(a?.id ?? "000"), 10);
183
+ const suffixB = parseInt(getIdSuffix(b?.id ?? "000"), 10);
184
+ return suffixA - suffixB;
185
+ });
186
+ if (capabilityIds.length === 0) {
187
+ output += "*(No user scenarios defined)*\n\n";
188
+ }
189
+ else {
190
+ for (const capability of capabilityIds) {
191
+ if (!capability)
192
+ continue;
193
+ const priority = extractPriority(capability);
194
+ output += `### User Story ${getIdSuffix(capability.id)} - ${capability.name} (Priority: ${priority})\n\n`;
195
+ if (capability.description) {
196
+ const desc = textToString(capability.description);
197
+ output += `${desc}\n\n`;
198
+ }
199
+ output += `**Why this priority**: [explanation needed]\n\n`;
200
+ // Independent Test section
201
+ if (capability.context) {
202
+ const context = textToString(capability.context);
203
+ output += `**Independent Test**: ${context}\n\n`;
204
+ }
205
+ else {
206
+ output += `**Independent Test**: [test description needed]\n\n`;
207
+ }
208
+ // Acceptance Scenarios
209
+ output += "**Acceptance Scenarios**:\n\n";
210
+ if (capability.description && Array.isArray(capability.description)) {
211
+ const scenarioLines = capability.description.filter((line) => line.toUpperCase().startsWith("**GIVEN**"));
212
+ if (scenarioLines.length > 0) {
213
+ scenarioLines.forEach((line, idx) => {
214
+ output += `${String(idx + 1)}. ${line}\n\n`;
215
+ });
216
+ }
217
+ else {
218
+ output +=
219
+ "1. **Given** [state], **When** [action], **Then** [outcome]\n\n";
220
+ }
221
+ }
222
+ else {
223
+ output +=
224
+ "1. **Given** [state], **When** [action], **Then** [outcome]\n\n";
225
+ }
226
+ output += "---\n\n";
227
+ }
228
+ }
229
+ // Edge Cases section
230
+ output += "### Edge Cases\n\n";
231
+ output += "- [edge case items]\n\n";
232
+ // Requirements section
233
+ output += "## Requirements *(mandatory)*\n\n";
234
+ // Functional Requirements
235
+ output += "### Functional Requirements\n\n";
236
+ const frNodes = findNodesByType(doc, "invariant").filter((n) => n.id.startsWith(`${prefix}-FR-`));
237
+ if (frNodes.length === 0) {
238
+ output += "- [requirements needed]\n\n";
239
+ }
240
+ else {
241
+ frNodes.forEach((req) => {
242
+ const reqText = textToString(req.description ?? "");
243
+ const status = req.status === "proposed" ? ` [NEEDS CLARIFICATION]` : "";
244
+ output += `- **${req.id}**: ${reqText}${status}\n`;
245
+ });
246
+ output += "\n";
247
+ }
248
+ // Key Entities
249
+ const entityNodes = findNodesByType(doc, "concept").filter((n) => n.id.startsWith(`${prefix}-ENT-`));
250
+ if (entityNodes.length > 0) {
251
+ output += "### Key Entities *(include if feature involves data)*\n\n";
252
+ for (const entity of entityNodes) {
253
+ output += `- **${entity.name}**: ${textToString(entity.description ?? "")}\n`;
254
+ }
255
+ output += "\n";
256
+ }
257
+ // Success Criteria section
258
+ output += "## Success Criteria *(mandatory)*\n\n";
259
+ output += "### Measurable Outcomes\n\n";
260
+ const scNodes = findNodesByType(doc, "invariant").filter((n) => n.id.startsWith(`${prefix}-SC-`));
261
+ if (scNodes.length === 0) {
262
+ output += "- **SC-001**: [metric needed]\n\n";
263
+ }
264
+ else {
265
+ for (const sc of scNodes) {
266
+ output += `- **${sc.id}**: ${textToString(sc.description ?? "")}\n`;
267
+ }
268
+ output += "\n";
269
+ }
270
+ return output.trim();
271
+ }
272
+ // ============================================================================
273
+ // generatePlan
274
+ // ============================================================================
275
+ export function generatePlan(doc, prefix) {
276
+ const implProtocolId = `${prefix}-PROT-IMPL`;
277
+ const protocol = findNode(doc, implProtocolId);
278
+ if (!protocol) {
279
+ return "";
280
+ }
281
+ const title = protocol.name;
282
+ let output = `# Implementation Plan: ${title}\n\n`;
283
+ output += `**Branch**: \`${prefix.toLowerCase()}\`\n`;
284
+ output += `**Date**: ${new Date().toISOString().split("T")[0]}\n`;
285
+ output += `**Spec**: [link to spec.md]\n\n`;
286
+ // Summary
287
+ output += "## Summary\n\n";
288
+ if (protocol.description) {
289
+ output += `${textToString(protocol.description)}\n\n`;
290
+ }
291
+ else {
292
+ output += "[Summary of the implementation plan]\n\n";
293
+ }
294
+ // Technical Context
295
+ output += "## Technical Context\n\n";
296
+ if (protocol.context) {
297
+ const context = textToString(protocol.context);
298
+ const lines = Array.isArray(protocol.context)
299
+ ? protocol.context
300
+ : [context];
301
+ lines.forEach((line) => {
302
+ output += `${line}\n`;
303
+ });
304
+ output += "\n";
305
+ }
306
+ else {
307
+ output += "[Key technical decisions and context]\n\n";
308
+ }
309
+ // Constitution Check
310
+ output += "## Constitution Check\n\n";
311
+ if (protocol.rationale) {
312
+ output += `${textToString(protocol.rationale)}\n\n`;
313
+ }
314
+ else {
315
+ output += "[Alignment with constitution principles]\n\n";
316
+ }
317
+ // Project Structure
318
+ output += "## Project Structure\n\n";
319
+ output += "[Project directory structure and module organization]\n\n";
320
+ return output.trim();
321
+ }
322
+ // ============================================================================
323
+ // generateTasks
324
+ // ============================================================================
325
+ export function generateTasks(doc, prefix) {
326
+ const implProtocolId = `${prefix}-PROT-IMPL`;
327
+ const protocol = findNode(doc, implProtocolId);
328
+ if (!protocol) {
329
+ return "";
330
+ }
331
+ const title = protocol.name;
332
+ let output = `# Task List: ${title}\n\n`;
333
+ // Extract subsystem from the protocol node.
334
+ if (!protocol.subsystem?.nodes || protocol.subsystem.nodes.length === 0) {
335
+ output += "*(No phases defined)*\n\n";
336
+ return output.trim();
337
+ }
338
+ const subsystem = protocol.subsystem;
339
+ // Helper functions to work with subsystem data
340
+ function findNodeInSubsystem(id) {
341
+ return subsystem.nodes.find((n) => n.id === id) ?? null;
342
+ }
343
+ function findRelationshipsFromInSubsystem(fromId, relationType) {
344
+ return (subsystem.relationships ?? []).filter((r) => {
345
+ if (r.from !== fromId)
346
+ return false;
347
+ if (relationType && r.type !== relationType)
348
+ return false;
349
+ return true;
350
+ });
351
+ }
352
+ function findRelationshipsToInSubsystem(toId, relationType) {
353
+ return (subsystem.relationships ?? []).filter((r) => {
354
+ if (r.to !== toId)
355
+ return false;
356
+ if (relationType && r.type !== relationType)
357
+ return false;
358
+ return true;
359
+ });
360
+ }
361
+ // Find top-level change nodes within the subsystem (phases are now change nodes).
362
+ const topLevelChangeIds = subsystem.nodes.filter((n) => n.type === "change" && !n.parent);
363
+ // Sort top-level changes by must_follow order using topological sort.
364
+ const sortedChanges = [];
365
+ const processedIds = new Set();
366
+ function addChangeInOrder(changeId) {
367
+ if (!changeId || processedIds.has(changeId))
368
+ return;
369
+ processedIds.add(changeId);
370
+ const change = findNodeInSubsystem(changeId);
371
+ if (change) {
372
+ sortedChanges.push(change);
373
+ }
374
+ // Find changes that must_follow this change (i.e., come after it).
375
+ // must_follow relationship: { from: nextChange, to: thisChange } means nextChange follows thisChange
376
+ const followersRels = findRelationshipsToInSubsystem(changeId, "must_follow");
377
+ for (const rel of followersRels) {
378
+ addChangeInOrder(rel.from);
379
+ }
380
+ }
381
+ // Start with changes that don't must_follow any other change (first changes).
382
+ // A change is first if it has no outgoing must_follow relationship.
383
+ for (const change of topLevelChangeIds) {
384
+ const precedingRels = findRelationshipsFromInSubsystem(change.id, "must_follow");
385
+ if (precedingRels.length === 0) {
386
+ addChangeInOrder(change.id);
387
+ }
388
+ }
389
+ // Add any remaining changes not yet processed
390
+ for (const change of topLevelChangeIds) {
391
+ if (!processedIds.has(change.id)) {
392
+ addChangeInOrder(change.id);
393
+ }
394
+ }
395
+ if (sortedChanges.length === 0) {
396
+ output += "*(No phases defined)*\n\n";
397
+ return output.trim();
398
+ }
399
+ // Recursive helper to render a change node and its tasks, including nested child changes.
400
+ // Render tasks from a change node's plan array.
401
+ function renderPlanItems(change, taskCounter) {
402
+ let result = "";
403
+ const tasks = parseTasks(change);
404
+ for (const task of tasks) {
405
+ // Find capability that this change implements.
406
+ let usStory = null;
407
+ const implRelsSubsystem = findRelationshipsFromInSubsystem(change.id, "implements");
408
+ if (implRelsSubsystem.length > 0) {
409
+ usStory = getIdSuffix(implRelsSubsystem[0].to);
410
+ }
411
+ else {
412
+ const implRelsDoc = findRelationshipsFrom(doc, change.id, "implements");
413
+ if (implRelsDoc.length > 0) {
414
+ usStory = getIdSuffix(implRelsDoc[0].to);
415
+ }
416
+ }
417
+ const checkbox = task.done ? "[x]" : "[ ]";
418
+ const taskNum = String(taskCounter.value).padStart(3, "0");
419
+ let taskLine = `- ${checkbox} T${taskNum}`;
420
+ if (usStory && usStory !== "000") {
421
+ taskLine += ` [US${usStory}]`;
422
+ }
423
+ taskLine += ` ${textToString(task.description)}`;
424
+ result += taskLine + "\n";
425
+ taskCounter.value++;
426
+ }
427
+ return result;
428
+ }
429
+ // Recursively render a change node and its children.
430
+ // Top-level changes get "Phase N:" prefix; nested children get just their name.
431
+ function renderChangeNode(change, headingLevel, taskCounter, phaseLabel) {
432
+ let result = "";
433
+ const heading = "#".repeat(headingLevel);
434
+ result += phaseLabel
435
+ ? `${heading} ${phaseLabel}: ${change.name}\n\n`
436
+ : `${heading} ${change.name}\n\n`;
437
+ result += renderPlanItems(change, taskCounter);
438
+ if (result.endsWith("\n"))
439
+ result += "\n";
440
+ else
441
+ result += "\n\n";
442
+ // Recurse into child change nodes.
443
+ if (change.subsystem?.nodes && change.subsystem.nodes.length > 0) {
444
+ const childChanges = change.subsystem.nodes.filter((n) => n.type === "change");
445
+ for (const childChange of childChanges) {
446
+ result += renderChangeNode(childChange, headingLevel + 1, taskCounter);
447
+ }
448
+ }
449
+ return result;
450
+ }
451
+ // Render each top-level change as a phase.
452
+ const taskCounter = { value: 1 };
453
+ for (let i = 0; i < sortedChanges.length; i++) {
454
+ const change = sortedChanges[i];
455
+ if (change) {
456
+ output += renderChangeNode(change, 2, taskCounter, `Phase ${String(i + 1)}`);
457
+ }
458
+ }
459
+ return output.trim();
460
+ }
461
+ // ============================================================================
462
+ // generateChecklist
463
+ // ============================================================================
464
+ export function generateChecklist(doc, prefix) {
465
+ const gateId = `${prefix}-CHK`;
466
+ const gate = findNode(doc, gateId);
467
+ if (!gate) {
468
+ return "";
469
+ }
470
+ const title = gate.name;
471
+ let output = `# Checklist: ${title}\n\n`;
472
+ output += `**Purpose**: ${textToString(gate.description ?? "No description provided")}\n`;
473
+ output += `**Created**: ${new Date().toISOString().split("T")[0]}\n\n`;
474
+ // Parse description for section markers and lifecycle for items
475
+ const description = gate.description ? textToString(gate.description) : "";
476
+ const lines = description.split("\n");
477
+ const sections = new Map();
478
+ let currentSection = "Items";
479
+ for (const line of lines) {
480
+ if (line.startsWith("## ")) {
481
+ currentSection = line.replace("## ", "").trim();
482
+ }
483
+ }
484
+ // Use lifecycle as checklist items
485
+ if (gate.lifecycle && Object.keys(gate.lifecycle).length > 0) {
486
+ for (const [key, done] of Object.entries(gate.lifecycle)) {
487
+ if (!sections.has(currentSection)) {
488
+ sections.set(currentSection, []);
489
+ }
490
+ const sectionItems = sections.get(currentSection);
491
+ if (sectionItems) {
492
+ sectionItems.push({ key, done });
493
+ }
494
+ }
495
+ }
496
+ if (sections.size === 0) {
497
+ output += "## Items\n\n";
498
+ output += "- [ ] CHK001 [checklist item needed]\n\n";
499
+ }
500
+ else {
501
+ let itemCounter = 1;
502
+ for (const [section, items] of sections) {
503
+ output += `## ${section}\n\n`;
504
+ for (const item of items) {
505
+ const checkbox = item.done ? "[x]" : "[ ]";
506
+ const checkNum = String(itemCounter).padStart(3, "0");
507
+ output += `- ${checkbox} CHK${checkNum} ${item.key}\n`;
508
+ itemCounter++;
509
+ }
510
+ output += "\n";
511
+ }
512
+ }
513
+ return output.trim();
514
+ }
515
+ // ============================================================================
516
+ // generateSpecKitProject
517
+ // ============================================================================
518
+ export function generateSpecKitProject(doc, outputDir, prefix) {
519
+ // Create output directory if it doesn't exist
520
+ mkdirSync(outputDir, { recursive: true });
521
+ // Generate and write constitution.md
522
+ const constitution = generateConstitution(doc, prefix);
523
+ if (constitution.trim()) {
524
+ writeFileSync(join(outputDir, "constitution.md"), constitution + "\n");
525
+ }
526
+ // Generate and write spec.md
527
+ const spec = generateSpec(doc, prefix);
528
+ if (spec.trim()) {
529
+ writeFileSync(join(outputDir, "spec.md"), spec + "\n");
530
+ }
531
+ // Generate and write plan.md
532
+ const plan = generatePlan(doc, prefix);
533
+ if (plan.trim()) {
534
+ writeFileSync(join(outputDir, "plan.md"), plan + "\n");
535
+ }
536
+ // Generate and write tasks.md
537
+ const tasks = generateTasks(doc, prefix);
538
+ if (tasks.trim()) {
539
+ writeFileSync(join(outputDir, "tasks.md"), tasks + "\n");
540
+ }
541
+ // Generate and write checklist.md
542
+ const checklist = generateChecklist(doc, prefix);
543
+ if (checklist.trim()) {
544
+ writeFileSync(join(outputDir, "checklist.md"), checklist + "\n");
545
+ }
546
+ }
@@ -0,0 +1,4 @@
1
+ export { detectSpecKitProject, listFeatures, getFeature, resolveConstitution, type SpecKitProject, type SpecKitFeature, } from "./project.js";
2
+ export { parseConstitution, parseSpec, parsePlan, parseTasks, parseChecklist, parseSpecKitFeature, type ParseResult, } from "./parse.js";
3
+ export { generateConstitution, generateSpec, generatePlan, generateTasks, generateChecklist, generateSpecKitProject, } from "./generate.js";
4
+ export { initDocument, addTask, planStatus, planProgress, checkGate, isTaskDone, countTasks, type PlanStatus, type PhaseProgress, type GateIssue, type GateResult, } from "./plan.js";
@@ -0,0 +1,4 @@
1
+ export { detectSpecKitProject, listFeatures, getFeature, resolveConstitution, } from "./project.js";
2
+ export { parseConstitution, parseSpec, parsePlan, parseTasks, parseChecklist, parseSpecKitFeature, } from "./parse.js";
3
+ export { generateConstitution, generateSpec, generatePlan, generateTasks, generateChecklist, generateSpecKitProject, } from "./generate.js";
4
+ export { initDocument, addTask, planStatus, planProgress, checkGate, isTaskDone, countTasks, } from "./plan.js";
@@ -0,0 +1,11 @@
1
+ import type { SysProMDocument, Node, Relationship } from "../schema.js";
2
+ export interface ParseResult {
3
+ nodes: Node[];
4
+ relationships: Relationship[];
5
+ }
6
+ export declare function parseConstitution(content: string, idPrefix: string): ParseResult;
7
+ export declare function parseSpec(content: string, idPrefix: string): ParseResult;
8
+ export declare function parsePlan(content: string, idPrefix: string): ParseResult;
9
+ export declare function parseTasks(content: string, idPrefix: string): ParseResult;
10
+ export declare function parseChecklist(content: string, idPrefix: string): ParseResult;
11
+ export declare function parseSpecKitFeature(featureDir: string, idPrefix: string, constitutionPath?: string): SysProMDocument;