sysprom 1.0.0 → 1.0.5

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,636 @@
1
+ import { textToString } from "../text.js";
2
+ // ============================================================================
3
+ // Helper functions
4
+ // ============================================================================
5
+ /**
6
+ * Find a single node by ID, or null if not found.
7
+ */
8
+ function findNode(doc, id) {
9
+ return doc.nodes.find((n) => n.id === id) ?? null;
10
+ }
11
+ /**
12
+ * Find a single node by ID in a subsystem, or null if not found.
13
+ */
14
+ function findNodeInSubsystem(subsystem, id) {
15
+ if (!subsystem)
16
+ return null;
17
+ return subsystem.nodes.find((n) => n.id === id) ?? null;
18
+ }
19
+ /**
20
+ * Find all nodes of a specific type.
21
+ */
22
+ function findNodesByType(doc, type) {
23
+ return doc.nodes.filter((n) => n.type === type);
24
+ }
25
+ /**
26
+ * Find all nodes of a specific type in a subsystem.
27
+ */
28
+ function findNodesByTypeInSubsystem(subsystem, type) {
29
+ if (!subsystem)
30
+ return [];
31
+ return subsystem.nodes.filter((n) => n.type === type);
32
+ }
33
+ /**
34
+ * Find relationships from a source node to nodes of a target type (within a subsystem).
35
+ */
36
+ function findRelationshipsFrom(subsystem, fromId, relationType) {
37
+ if (!subsystem)
38
+ return [];
39
+ return (subsystem.relationships ?? []).filter((r) => {
40
+ if (r.from !== fromId)
41
+ return false;
42
+ if (relationType && r.type !== relationType)
43
+ return false;
44
+ return true;
45
+ });
46
+ }
47
+ /**
48
+ * Find relationships to a target node (within a subsystem).
49
+ */
50
+ function findRelationshipsTo(subsystem, toId, relationType) {
51
+ if (!subsystem)
52
+ return [];
53
+ return (subsystem.relationships ?? []).filter((r) => {
54
+ if (r.to !== toId)
55
+ return false;
56
+ if (relationType && r.type !== relationType)
57
+ return false;
58
+ return true;
59
+ });
60
+ }
61
+ /**
62
+ * Detect if a text contains non-placeholder acceptance criteria.
63
+ * Looks for GIVEN/WHEN/THEN patterns (case-insensitive).
64
+ */
65
+ function hasAcceptanceCriteria(description) {
66
+ if (!description)
67
+ return false;
68
+ const text = textToString(description).toLowerCase();
69
+ return /\b(given|when|then)\b/.test(text);
70
+ }
71
+ /**
72
+ * Sort change nodes topologically using must_follow relationships.
73
+ */
74
+ function sortChangesByOrder(subsystem, changeNodes) {
75
+ const subsystemToUse = subsystem ?? { nodes: [], relationships: [] };
76
+ const sorted = [];
77
+ const processedIds = new Set();
78
+ function addChangeInOrder(changeId) {
79
+ if (!changeId || processedIds.has(changeId))
80
+ return;
81
+ processedIds.add(changeId);
82
+ const change = findNodeInSubsystem(subsystemToUse, changeId);
83
+ if (change) {
84
+ sorted.push(change);
85
+ }
86
+ // Find changes that must_follow this change (i.e., come after it)
87
+ const followersRels = findRelationshipsTo(subsystemToUse, changeId, "must_follow");
88
+ for (const rel of followersRels) {
89
+ addChangeInOrder(rel.from);
90
+ }
91
+ }
92
+ // Start with changes that don't must_follow any other change (first changes)
93
+ for (const change of changeNodes) {
94
+ const precedingRels = findRelationshipsFrom(subsystemToUse, change.id, "must_follow");
95
+ if (precedingRels.length === 0) {
96
+ addChangeInOrder(change.id);
97
+ }
98
+ }
99
+ // Add any remaining changes not yet processed
100
+ for (const change of changeNodes) {
101
+ if (!processedIds.has(change.id)) {
102
+ addChangeInOrder(change.id);
103
+ }
104
+ }
105
+ return sorted;
106
+ }
107
+ // ============================================================================
108
+ // initDocument
109
+ // ============================================================================
110
+ /**
111
+ * Scaffold a new SysProMDocument with the standard spec-kit-compatible node
112
+ * structure for a given prefix and name.
113
+ *
114
+ * Creates four skeleton nodes:
115
+ * - {prefix}-CONST protocol (constitution)
116
+ * - {prefix}-SPEC artefact (specification)
117
+ * - {prefix}-PROT-IMPL protocol (implementation plan) — with empty subsystem
118
+ * - {prefix}-CHK gate (checklist)
119
+ *
120
+ * Relationships wired:
121
+ * - {prefix}-SPEC governed_by {prefix}-CONST
122
+ * - {prefix}-CHK governed_by {prefix}-PROT-IMPL
123
+ *
124
+ * Tasks are not pre-scaffolded; use addTask to add them.
125
+ */
126
+ export function initDocument(prefix, name) {
127
+ const nodes = [
128
+ {
129
+ id: `${prefix}-CONST`,
130
+ type: "protocol",
131
+ name: `${name} Constitution`,
132
+ description: "[Constitution content needed]",
133
+ },
134
+ {
135
+ id: `${prefix}-SPEC`,
136
+ type: "artefact",
137
+ name: `${name} Specification`,
138
+ status: "proposed",
139
+ },
140
+ {
141
+ id: `${prefix}-PROT-IMPL`,
142
+ type: "protocol",
143
+ name: `${name} Implementation Plan`,
144
+ subsystem: {
145
+ nodes: [],
146
+ relationships: [],
147
+ },
148
+ },
149
+ {
150
+ id: `${prefix}-CHK`,
151
+ type: "gate",
152
+ name: `${name} Checklist`,
153
+ lifecycle: {},
154
+ },
155
+ ];
156
+ const relationships = [
157
+ {
158
+ from: `${prefix}-SPEC`,
159
+ to: `${prefix}-CONST`,
160
+ type: "governed_by",
161
+ },
162
+ {
163
+ from: `${prefix}-CHK`,
164
+ to: `${prefix}-PROT-IMPL`,
165
+ type: "governed_by",
166
+ },
167
+ ];
168
+ return {
169
+ metadata: {
170
+ title: name,
171
+ doc_type: "speckit",
172
+ },
173
+ nodes,
174
+ relationships,
175
+ };
176
+ }
177
+ // ============================================================================
178
+ // addTask
179
+ // ============================================================================
180
+ /**
181
+ * Immutably add a new task (change node) to PROT-IMPL.subsystem or to a parent
182
+ * change node's subsystem.
183
+ *
184
+ * - If parentId is not provided: adds CHG-{N} to PROT-IMPL.subsystem
185
+ * (where N = count of existing change nodes + 1)
186
+ * - If parentId is provided: recursively finds parent change node, adds {parentId}-{M}
187
+ * to parent's subsystem (creating subsystem if needed, where M = count of existing
188
+ * change children + 1)
189
+ *
190
+ * Wires must_follow to previous sibling change node at the same level.
191
+ * Default name: "Task N".
192
+ */
193
+ export function addTask(doc, prefix, name, parentId) {
194
+ const protImpl = findNode(doc, `${prefix}-PROT-IMPL`);
195
+ if (!protImpl) {
196
+ throw new Error(`Node ${prefix}-PROT-IMPL not found`);
197
+ }
198
+ if (!parentId) {
199
+ // Add to PROT-IMPL.subsystem as a top-level task
200
+ const subsystem = protImpl.subsystem ?? { nodes: [], relationships: [] };
201
+ const existingChanges = subsystem.nodes.filter((n) => n.type === "change");
202
+ const taskNum = existingChanges.length + 1;
203
+ const taskName = name ?? `Task ${String(taskNum)}`;
204
+ const changeId = `CHG-${String(taskNum)}`;
205
+ const newChange = {
206
+ id: changeId,
207
+ type: "change",
208
+ name: taskName,
209
+ plan: [],
210
+ };
211
+ // Build new relationships
212
+ const newRels = [];
213
+ // If not the first task, add must_follow from previous task
214
+ if (taskNum > 1) {
215
+ const prevTaskId = `CHG-${String(taskNum - 1)}`;
216
+ newRels.push({
217
+ from: changeId,
218
+ to: prevTaskId,
219
+ type: "must_follow",
220
+ });
221
+ }
222
+ // Merge into subsystem
223
+ const updatedSubsystem = {
224
+ ...(subsystem.metadata ? { metadata: subsystem.metadata } : {}),
225
+ nodes: [...subsystem.nodes, newChange],
226
+ relationships: [...(subsystem.relationships ?? []), ...newRels],
227
+ ...(subsystem.external_references
228
+ ? { external_references: subsystem.external_references }
229
+ : {}),
230
+ };
231
+ // Update the protocol node
232
+ const updatedProtImpl = {
233
+ ...protImpl,
234
+ subsystem: updatedSubsystem,
235
+ };
236
+ // Update the document
237
+ const updatedNodes = doc.nodes.map((n) => n.id === protImpl.id ? updatedProtImpl : n);
238
+ return {
239
+ ...doc,
240
+ nodes: updatedNodes,
241
+ };
242
+ }
243
+ else {
244
+ // Add to parent change node's subsystem
245
+ return addTaskToParent(doc, protImpl, prefix, parentId, name);
246
+ }
247
+ }
248
+ /**
249
+ * Helper function to recursively add a task to a parent change node's subsystem.
250
+ */
251
+ function addTaskToParent(doc, protImpl, prefix, parentId, name) {
252
+ // Find the parent change node in the subsystem tree
253
+ function findParentAndAddTask(subsystem) {
254
+ if (!subsystem) {
255
+ return { found: false, updatedSubsystem: undefined };
256
+ }
257
+ // Check if parent exists at this level
258
+ const parentNode = subsystem.nodes.find((n) => n.id === parentId);
259
+ if (parentNode?.type === "change") {
260
+ // Found the parent, add task to its subsystem
261
+ const parentSubsystem = parentNode.subsystem ?? {
262
+ nodes: [],
263
+ relationships: [],
264
+ };
265
+ const existingChildren = parentSubsystem.nodes.filter((n) => n.type === "change");
266
+ const childNum = existingChildren.length + 1;
267
+ const childName = name ?? `Task ${String(childNum)}`;
268
+ const changeId = `${parentId}-${String(childNum)}`;
269
+ const newChange = {
270
+ id: changeId,
271
+ type: "change",
272
+ name: childName,
273
+ plan: [],
274
+ };
275
+ // Build new relationships for child
276
+ const newRels = [];
277
+ // If not the first child, add must_follow from previous child
278
+ if (childNum > 1) {
279
+ const prevChildId = `${parentId}-${String(childNum - 1)}`;
280
+ newRels.push({
281
+ from: changeId,
282
+ to: prevChildId,
283
+ type: "must_follow",
284
+ });
285
+ }
286
+ // Update parent's subsystem
287
+ const updatedParentSubsystem = {
288
+ ...(parentSubsystem.metadata
289
+ ? { metadata: parentSubsystem.metadata }
290
+ : {}),
291
+ nodes: [...parentSubsystem.nodes, newChange],
292
+ relationships: [...(parentSubsystem.relationships ?? []), ...newRels],
293
+ ...(parentSubsystem.external_references
294
+ ? { external_references: parentSubsystem.external_references }
295
+ : {}),
296
+ };
297
+ // Update parent node
298
+ const updatedParent = {
299
+ ...parentNode,
300
+ subsystem: updatedParentSubsystem,
301
+ };
302
+ // Update subsystem nodes
303
+ const updatedNodes = subsystem.nodes.map((n) => n.id === parentId ? updatedParent : n);
304
+ return {
305
+ found: true,
306
+ updatedSubsystem: {
307
+ ...(subsystem.metadata ? { metadata: subsystem.metadata } : {}),
308
+ nodes: updatedNodes,
309
+ relationships: subsystem.relationships ?? undefined,
310
+ ...(subsystem.external_references
311
+ ? { external_references: subsystem.external_references }
312
+ : {}),
313
+ },
314
+ };
315
+ }
316
+ // Recursively search in child subsystems
317
+ const updatesAndNodes = {
318
+ updated: false,
319
+ nodes: [],
320
+ };
321
+ for (const n of subsystem.nodes) {
322
+ if (n.type === "change" && n.subsystem) {
323
+ const { found, updatedSubsystem: childUpdated } = findParentAndAddTask(n.subsystem);
324
+ if (found) {
325
+ updatesAndNodes.updated = true;
326
+ updatesAndNodes.nodes.push({
327
+ ...n,
328
+ subsystem: childUpdated,
329
+ });
330
+ }
331
+ else {
332
+ updatesAndNodes.nodes.push(n);
333
+ }
334
+ }
335
+ else {
336
+ updatesAndNodes.nodes.push(n);
337
+ }
338
+ }
339
+ return updatesAndNodes.updated
340
+ ? {
341
+ found: true,
342
+ updatedSubsystem: {
343
+ ...(subsystem.metadata ? { metadata: subsystem.metadata } : {}),
344
+ nodes: updatesAndNodes.nodes,
345
+ relationships: subsystem.relationships ?? undefined,
346
+ ...(subsystem.external_references
347
+ ? { external_references: subsystem.external_references }
348
+ : {}),
349
+ },
350
+ }
351
+ : { found: false, updatedSubsystem: subsystem };
352
+ }
353
+ const { found, updatedSubsystem } = findParentAndAddTask(protImpl.subsystem);
354
+ if (!found) {
355
+ throw new Error(`Parent change node ${parentId} not found`);
356
+ }
357
+ // Update the protocol node
358
+ const updatedProtImpl = {
359
+ ...protImpl,
360
+ subsystem: updatedSubsystem,
361
+ };
362
+ // Update the document
363
+ const updatedNodes = doc.nodes.map((n) => n.id === protImpl.id ? updatedProtImpl : n);
364
+ return {
365
+ ...doc,
366
+ nodes: updatedNodes,
367
+ };
368
+ }
369
+ // ============================================================================
370
+ // isTaskDone
371
+ // ============================================================================
372
+ /**
373
+ * Check if a change node's task is complete.
374
+ *
375
+ * If no subsystem or no change children in subsystem:
376
+ * - All items in node.plan must have done === true AND at least one item must exist
377
+ * If subsystem has change children:
378
+ * - All children must be recursively done AND own plan items (if any) must be done
379
+ */
380
+ export function isTaskDone(node) {
381
+ // If the node has a subsystem with change children, check those recursively
382
+ if (node.subsystem) {
383
+ const changeChildren = node.subsystem.nodes.filter((n) => n.type === "change");
384
+ if (changeChildren.length > 0) {
385
+ // All change children must be done, and own plan items must be done
386
+ const allChildrenDone = changeChildren.every((child) => isTaskDone(child));
387
+ const ownPlanDone = (node.plan ?? []).length === 0 ||
388
+ (node.plan ?? []).every((item) => item.done === true);
389
+ return allChildrenDone && ownPlanDone;
390
+ }
391
+ }
392
+ // No subsystem or no change children: check own plan
393
+ const planItems = node.plan ?? [];
394
+ if (planItems.length === 0) {
395
+ return false;
396
+ }
397
+ return planItems.every((item) => item.done === true);
398
+ }
399
+ // ============================================================================
400
+ // countTasks
401
+ // ============================================================================
402
+ /**
403
+ * Count total and completed tasks within a change node.
404
+ *
405
+ * Sums plan[] items from this node and recursively from all change nodes in
406
+ * subsystem (and their subsystems).
407
+ */
408
+ export function countTasks(node) {
409
+ let total = 0;
410
+ let done = 0;
411
+ // Count own plan items
412
+ const ownPlan = node.plan ?? [];
413
+ total += ownPlan.length;
414
+ done += ownPlan.filter((item) => item.done === true).length;
415
+ // Recursively count from change children in subsystem
416
+ if (node.subsystem) {
417
+ const changeChildren = node.subsystem.nodes.filter((n) => n.type === "change");
418
+ for (const child of changeChildren) {
419
+ const childCount = countTasks(child);
420
+ total += childCount.total;
421
+ done += childCount.done;
422
+ }
423
+ }
424
+ return { total, done };
425
+ }
426
+ // ============================================================================
427
+ // planStatus
428
+ // ============================================================================
429
+ /**
430
+ * Inspect a document and return workflow completeness for a given prefix.
431
+ * Never throws — missing nodes are reported as "not defined".
432
+ */
433
+ export function planStatus(doc, prefix) {
434
+ const constitution = findNode(doc, `${prefix}-CONST`);
435
+ const spec = findNode(doc, `${prefix}-SPEC`);
436
+ const protImpl = findNode(doc, `${prefix}-PROT-IMPL`);
437
+ const checklist = findNode(doc, `${prefix}-CHK`);
438
+ const userStories = findNodesByType(doc, "capability").filter((n) => n.id.startsWith(`${prefix}-US-`));
439
+ const storiesNeedingAcceptanceCriteria = userStories
440
+ .filter((us) => !hasAcceptanceCriteria(us.description))
441
+ .map((us) => us.id);
442
+ // Count phases (top-level change nodes)
443
+ const phaseCount = (protImpl?.subsystem?.nodes ?? []).filter((n) => n.type === "change").length;
444
+ // Count tasks using the helper
445
+ let totalTasks = 0;
446
+ let doneTasks = 0;
447
+ const changeNodes = (protImpl?.subsystem?.nodes ?? []).filter((n) => n.type === "change");
448
+ for (const change of changeNodes) {
449
+ const taskCount = countTasks(change);
450
+ totalTasks += taskCount.total;
451
+ doneTasks += taskCount.done;
452
+ }
453
+ // Checklist stats
454
+ const checklistLifecycle = checklist?.lifecycle ?? {};
455
+ const checklistItemCount = Object.keys(checklistLifecycle).length;
456
+ const checklistDoneCount = Object.values(checklistLifecycle).filter((v) => !!v).length;
457
+ // Determine nextStep
458
+ let nextStep;
459
+ if (!constitution) {
460
+ nextStep = `Define the constitution: run \`spm plan init\``;
461
+ }
462
+ else if (!spec) {
463
+ nextStep = `Define the specification: add a ${prefix}-SPEC artefact node`;
464
+ }
465
+ else if (userStories.length === 0) {
466
+ nextStep = `Add user stories: run \`spm add ${prefix} capability --id US-001 ...\``;
467
+ }
468
+ else if (storiesNeedingAcceptanceCriteria.length > 0) {
469
+ nextStep = `Fill in acceptance criteria for: ${storiesNeedingAcceptanceCriteria.join(", ")}`;
470
+ }
471
+ else if (!protImpl) {
472
+ nextStep = `Define the implementation plan: run \`spm add ${prefix} protocol --id PROT-IMPL ...\``;
473
+ }
474
+ else if (phaseCount === 0) {
475
+ nextStep = `Add tasks: run \`spm plan add-task <doc> --prefix ${prefix}\``;
476
+ }
477
+ else if (totalTasks === 0) {
478
+ nextStep = `Add tasks to the change nodes`;
479
+ }
480
+ else if (doneTasks < totalTasks) {
481
+ const remaining = totalTasks - doneTasks;
482
+ nextStep = `Complete remaining tasks (${String(remaining)} of ${String(totalTasks)} remaining)`;
483
+ }
484
+ else if (!checklist) {
485
+ nextStep = `Add a checklist gate node: ${prefix}-CHK`;
486
+ }
487
+ else if (checklistDoneCount < checklistItemCount) {
488
+ const remaining = checklistItemCount - checklistDoneCount;
489
+ nextStep = `Complete the checklist (${String(remaining)} of ${String(checklistItemCount)} items remaining)`;
490
+ }
491
+ else {
492
+ nextStep = `All steps complete`;
493
+ }
494
+ return {
495
+ constitution: {
496
+ defined: constitution !== null,
497
+ principleCount: constitution
498
+ ? findNodesByType(doc, "principle").filter((p) => (doc.relationships ?? []).some((r) => r.from === p.id &&
499
+ r.to === constitution.id &&
500
+ r.type === "part_of")).length
501
+ : 0,
502
+ },
503
+ spec: {
504
+ defined: spec !== null,
505
+ userStoryCount: userStories.length,
506
+ storiesNeedingAcceptanceCriteria,
507
+ },
508
+ plan: {
509
+ defined: protImpl !== null,
510
+ phaseCount,
511
+ },
512
+ tasks: {
513
+ total: totalTasks,
514
+ done: doneTasks,
515
+ },
516
+ checklist: {
517
+ defined: checklist !== null,
518
+ total: checklistItemCount,
519
+ done: checklistDoneCount,
520
+ },
521
+ nextStep,
522
+ };
523
+ }
524
+ // ============================================================================
525
+ // planProgress
526
+ // ============================================================================
527
+ /**
528
+ * Return per-task completion data.
529
+ * Tasks (change nodes) are discovered from PROT-IMPL.subsystem, sorted topologically.
530
+ */
531
+ export function planProgress(doc, prefix) {
532
+ const protImpl = findNode(doc, `${prefix}-PROT-IMPL`);
533
+ if (!protImpl) {
534
+ return [];
535
+ }
536
+ const subsystem = protImpl.subsystem;
537
+ const taskNodes = findNodesByTypeInSubsystem(subsystem, "change");
538
+ const sortedTasks = sortChangesByOrder(subsystem, taskNodes);
539
+ const result = [];
540
+ for (let i = 0; i < sortedTasks.length; i++) {
541
+ const task = sortedTasks[i];
542
+ const taskNum = i + 1;
543
+ // Count tasks for this change node
544
+ const taskCount = countTasks(task);
545
+ const percent = taskCount.total === 0
546
+ ? 0
547
+ : Math.round((taskCount.done / taskCount.total) * 100);
548
+ result.push({
549
+ phase: taskNum,
550
+ name: task.name,
551
+ done: taskCount.done,
552
+ total: taskCount.total,
553
+ percent,
554
+ });
555
+ }
556
+ return result;
557
+ }
558
+ // ============================================================================
559
+ // checkGate
560
+ // ============================================================================
561
+ /**
562
+ * Validate readiness to enter the given phase (1-indexed).
563
+ *
564
+ * Always checks:
565
+ * - Each capability ({prefix}-US-*) has a change node that implements it
566
+ * - Each capability has non-placeholder acceptance criteria
567
+ * - Each invariant ({prefix}-FR-*) has a change node that implements it
568
+ *
569
+ * Additionally for phase N > 1:
570
+ * - All tasks in phase N-1 must be done
571
+ */
572
+ export function checkGate(doc, prefix, phase) {
573
+ if (phase < 1) {
574
+ throw new Error("Phase must be >= 1");
575
+ }
576
+ const protImpl = findNode(doc, `${prefix}-PROT-IMPL`);
577
+ const subsystem = protImpl?.subsystem;
578
+ const issues = [];
579
+ // For phase N > 1, check that all tasks in phase N-1 are done
580
+ if (phase > 1) {
581
+ const taskNodes = findNodesByTypeInSubsystem(subsystem, "change");
582
+ const sortedTasks = sortChangesByOrder(subsystem, taskNodes);
583
+ if (phase - 1 <= sortedTasks.length) {
584
+ const prevTask = sortedTasks[phase - 2]; // 0-indexed
585
+ const taskCount = countTasks(prevTask);
586
+ const remaining = taskCount.total - taskCount.done;
587
+ if (remaining > 0) {
588
+ issues.push({
589
+ kind: "previous_tasks_incomplete",
590
+ phase: phase - 1,
591
+ remaining,
592
+ });
593
+ }
594
+ }
595
+ }
596
+ // Check user stories
597
+ const userStories = findNodesByType(doc, "capability").filter((n) => n.id.startsWith(`${prefix}-US-`));
598
+ for (const us of userStories) {
599
+ // Check if there's a change implementing it
600
+ const hasChange = (doc.relationships ?? []).some((r) => r.type === "implements" &&
601
+ r.to === us.id &&
602
+ r.from.startsWith(`${prefix}-CHG-`));
603
+ if (!hasChange) {
604
+ issues.push({
605
+ kind: "user_story_no_change",
606
+ storyId: us.id,
607
+ });
608
+ }
609
+ // Check if it has acceptance criteria
610
+ if (!hasAcceptanceCriteria(us.description)) {
611
+ issues.push({
612
+ kind: "user_story_no_acceptance_criteria",
613
+ storyId: us.id,
614
+ });
615
+ }
616
+ }
617
+ // Check functional requirements
618
+ const frs = findNodesByType(doc, "invariant").filter((n) => n.id.startsWith(`${prefix}-FR-`));
619
+ for (const fr of frs) {
620
+ // Check if there's a change implementing it
621
+ const hasChange = (doc.relationships ?? []).some((r) => r.type === "implements" &&
622
+ r.to === fr.id &&
623
+ r.from.startsWith(`${prefix}-CHG-`));
624
+ if (!hasChange) {
625
+ issues.push({
626
+ kind: "fr_no_change",
627
+ frId: fr.id,
628
+ });
629
+ }
630
+ }
631
+ return {
632
+ phase,
633
+ ready: issues.length === 0,
634
+ issues,
635
+ };
636
+ }
@@ -0,0 +1,39 @@
1
+ export interface SpecKitProject {
2
+ root: string;
3
+ specifyDir: string | null;
4
+ specsDir: string | null;
5
+ constitutionPath: string | null;
6
+ }
7
+ export interface SpecKitFeature {
8
+ id: string;
9
+ number: number;
10
+ name: string;
11
+ dir: string;
12
+ files: {
13
+ spec: string | null;
14
+ plan: string | null;
15
+ tasks: string | null;
16
+ checklist: string | null;
17
+ research: string | null;
18
+ dataModel: string | null;
19
+ quickstart: string | null;
20
+ };
21
+ }
22
+ /**
23
+ * Detect Spec-Kit project structure from a directory.
24
+ * Looks for .specify/ and specs/ subdirectories.
25
+ */
26
+ export declare function detectSpecKitProject(dir: string): SpecKitProject;
27
+ /**
28
+ * List all features in the specs/ directory, sorted by number.
29
+ */
30
+ export declare function listFeatures(project: SpecKitProject): SpecKitFeature[];
31
+ /**
32
+ * Get a specific feature by number or name.
33
+ * Matches "001", "001-feature-name", or "feature-name".
34
+ */
35
+ export declare function getFeature(project: SpecKitProject, idOrName: string): SpecKitFeature | null;
36
+ /**
37
+ * Resolve the constitution.md file, checking .specify/memory/ first, then root.
38
+ */
39
+ export declare function resolveConstitution(project: SpecKitProject): string | null;