sysprom 1.0.0 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -0
- package/dist/schema.json +510 -0
- package/dist/src/canonical-json.d.ts +23 -0
- package/dist/src/canonical-json.js +120 -0
- package/dist/src/cli/commands/add.d.ts +22 -0
- package/dist/src/cli/commands/add.js +95 -0
- package/dist/src/cli/commands/check.d.ts +10 -0
- package/dist/src/cli/commands/check.js +33 -0
- package/dist/src/cli/commands/graph.d.ts +15 -0
- package/dist/src/cli/commands/graph.js +32 -0
- package/dist/src/cli/commands/init.d.ts +2 -0
- package/dist/src/cli/commands/init.js +44 -0
- package/dist/src/cli/commands/json2md.d.ts +2 -0
- package/dist/src/cli/commands/json2md.js +60 -0
- package/dist/src/cli/commands/md2json.d.ts +2 -0
- package/dist/src/cli/commands/md2json.js +29 -0
- package/dist/src/cli/commands/plan.d.ts +2 -0
- package/dist/src/cli/commands/plan.js +227 -0
- package/dist/src/cli/commands/query.d.ts +2 -0
- package/dist/src/cli/commands/query.js +275 -0
- package/dist/src/cli/commands/remove.d.ts +13 -0
- package/dist/src/cli/commands/remove.js +50 -0
- package/dist/src/cli/commands/rename.d.ts +14 -0
- package/dist/src/cli/commands/rename.js +34 -0
- package/dist/src/cli/commands/search.d.ts +11 -0
- package/dist/src/cli/commands/search.js +37 -0
- package/dist/src/cli/commands/speckit.d.ts +2 -0
- package/dist/src/cli/commands/speckit.js +318 -0
- package/dist/src/cli/commands/stats.d.ts +10 -0
- package/dist/src/cli/commands/stats.js +51 -0
- package/dist/src/cli/commands/task.d.ts +2 -0
- package/dist/src/cli/commands/task.js +162 -0
- package/dist/src/cli/commands/update.d.ts +2 -0
- package/dist/src/cli/commands/update.js +219 -0
- package/dist/src/cli/commands/validate.d.ts +10 -0
- package/dist/src/cli/commands/validate.js +30 -0
- package/dist/src/cli/define-command.d.ts +34 -0
- package/dist/src/cli/define-command.js +237 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +3 -0
- package/dist/src/cli/program.d.ts +4 -0
- package/dist/src/cli/program.js +46 -0
- package/dist/src/cli/shared.d.ts +26 -0
- package/dist/src/cli/shared.js +41 -0
- package/dist/src/generate-schema.d.ts +1 -0
- package/dist/src/generate-schema.js +9 -0
- package/dist/src/index.d.ts +48 -0
- package/dist/src/index.js +99 -0
- package/dist/src/io.d.ts +22 -0
- package/dist/src/io.js +66 -0
- package/dist/src/json-to-md.d.ts +26 -0
- package/dist/src/json-to-md.js +498 -0
- package/dist/src/md-to-json.d.ts +22 -0
- package/dist/src/md-to-json.js +548 -0
- package/dist/src/operations/add-node.d.ts +887 -0
- package/dist/src/operations/add-node.js +21 -0
- package/dist/src/operations/add-plan-task.d.ts +594 -0
- package/dist/src/operations/add-plan-task.js +25 -0
- package/dist/src/operations/add-relationship.d.ts +635 -0
- package/dist/src/operations/add-relationship.js +25 -0
- package/dist/src/operations/check.d.ts +301 -0
- package/dist/src/operations/check.js +66 -0
- package/dist/src/operations/define-operation.d.ts +14 -0
- package/dist/src/operations/define-operation.js +21 -0
- package/dist/src/operations/graph.d.ts +303 -0
- package/dist/src/operations/graph.js +71 -0
- package/dist/src/operations/index.d.ts +38 -0
- package/dist/src/operations/index.js +45 -0
- package/dist/src/operations/init-document.d.ts +299 -0
- package/dist/src/operations/init-document.js +26 -0
- package/dist/src/operations/json-to-markdown.d.ts +298 -0
- package/dist/src/operations/json-to-markdown.js +13 -0
- package/dist/src/operations/mark-task-done.d.ts +594 -0
- package/dist/src/operations/mark-task-done.js +26 -0
- package/dist/src/operations/mark-task-undone.d.ts +594 -0
- package/dist/src/operations/mark-task-undone.js +26 -0
- package/dist/src/operations/markdown-to-json.d.ts +298 -0
- package/dist/src/operations/markdown-to-json.js +13 -0
- package/dist/src/operations/next-id.d.ts +322 -0
- package/dist/src/operations/next-id.js +29 -0
- package/dist/src/operations/node-history.d.ts +313 -0
- package/dist/src/operations/node-history.js +55 -0
- package/dist/src/operations/plan-add-task.d.ts +595 -0
- package/dist/src/operations/plan-add-task.js +18 -0
- package/dist/src/operations/plan-gate.d.ts +351 -0
- package/dist/src/operations/plan-gate.js +41 -0
- package/dist/src/operations/plan-init.d.ts +299 -0
- package/dist/src/operations/plan-init.js +17 -0
- package/dist/src/operations/plan-progress.d.ts +313 -0
- package/dist/src/operations/plan-progress.js +23 -0
- package/dist/src/operations/plan-status.d.ts +349 -0
- package/dist/src/operations/plan-status.js +41 -0
- package/dist/src/operations/query-node.d.ts +1065 -0
- package/dist/src/operations/query-node.js +27 -0
- package/dist/src/operations/query-nodes.d.ts +594 -0
- package/dist/src/operations/query-nodes.js +23 -0
- package/dist/src/operations/query-relationships.d.ts +343 -0
- package/dist/src/operations/query-relationships.js +27 -0
- package/dist/src/operations/remove-node.d.ts +895 -0
- package/dist/src/operations/remove-node.js +58 -0
- package/dist/src/operations/remove-relationship.d.ts +622 -0
- package/dist/src/operations/remove-relationship.js +26 -0
- package/dist/src/operations/rename.d.ts +594 -0
- package/dist/src/operations/rename.js +113 -0
- package/dist/src/operations/search.d.ts +593 -0
- package/dist/src/operations/search.js +39 -0
- package/dist/src/operations/speckit-diff.d.ts +330 -0
- package/dist/src/operations/speckit-diff.js +89 -0
- package/dist/src/operations/speckit-export.d.ts +300 -0
- package/dist/src/operations/speckit-export.js +17 -0
- package/dist/src/operations/speckit-import.d.ts +299 -0
- package/dist/src/operations/speckit-import.js +39 -0
- package/dist/src/operations/speckit-sync.d.ts +900 -0
- package/dist/src/operations/speckit-sync.js +116 -0
- package/dist/src/operations/state-at.d.ts +309 -0
- package/dist/src/operations/state-at.js +53 -0
- package/dist/src/operations/stats.d.ts +324 -0
- package/dist/src/operations/stats.js +85 -0
- package/dist/src/operations/task-list.d.ts +305 -0
- package/dist/src/operations/task-list.js +44 -0
- package/dist/src/operations/timeline.d.ts +312 -0
- package/dist/src/operations/timeline.js +46 -0
- package/dist/src/operations/trace-from-node.d.ts +1197 -0
- package/dist/src/operations/trace-from-node.js +36 -0
- package/dist/src/operations/update-metadata.d.ts +593 -0
- package/dist/src/operations/update-metadata.js +18 -0
- package/dist/src/operations/update-node.d.ts +957 -0
- package/dist/src/operations/update-node.js +24 -0
- package/dist/src/operations/update-plan-task.d.ts +595 -0
- package/dist/src/operations/update-plan-task.js +31 -0
- package/dist/src/operations/validate.d.ts +310 -0
- package/dist/src/operations/validate.js +82 -0
- package/dist/src/schema.d.ts +891 -0
- package/dist/src/schema.js +356 -0
- package/dist/src/speckit/generate.d.ts +7 -0
- package/dist/src/speckit/generate.js +546 -0
- package/dist/src/speckit/index.d.ts +4 -0
- package/dist/src/speckit/index.js +4 -0
- package/dist/src/speckit/parse.d.ts +11 -0
- package/dist/src/speckit/parse.js +712 -0
- package/dist/src/speckit/plan.d.ts +125 -0
- package/dist/src/speckit/plan.js +636 -0
- package/dist/src/speckit/project.d.ts +39 -0
- package/dist/src/speckit/project.js +141 -0
- package/dist/src/text.d.ts +23 -0
- package/dist/src/text.js +32 -0
- package/package.json +86 -8
- package/schema.json +510 -0
|
@@ -0,0 +1,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;
|