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