specdrive-cli 0.1.10 → 0.1.12
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 +955 -697
- package/agents/00-onboarding.md +261 -0
- package/agents/01-constitution.md +214 -201
- package/agents/02-specification.md +249 -226
- package/agents/03-uiux.md +156 -144
- package/agents/04-cascade.md +151 -122
- package/agents/05-discover-skills.md +136 -136
- package/agents/06-documentation.md +158 -145
- package/agents/07-implementation.md +201 -169
- package/agents/08-performance.md +179 -165
- package/agents/09-review-complete.md +239 -168
- package/agents/10-security.md +180 -167
- package/agents/11-test.md +195 -0
- package/commands/gates.js +73 -73
- package/commands/manifest.json +113 -95
- package/commands/permissions.json +39 -0
- package/commands/router.js +151 -127
- package/commands/tools.json +19 -19
- package/dashboard/app.js +394 -0
- package/dashboard/index.html +74 -0
- package/dashboard/server.js +166 -0
- package/dashboard/style.css +157 -0
- package/mcp/mcp.json +31 -0
- package/mcp/server.js +108 -0
- package/package.json +35 -32
- package/schemas/config.schema.json +20 -0
- package/schemas/workflow-state.schema.json +149 -38
- package/scripts/anti-redundancy.js +176 -176
- package/scripts/audit-log.js +46 -46
- package/scripts/check-permission.js +87 -0
- package/scripts/diff-spec.js +50 -50
- package/scripts/diff-version.js +96 -0
- package/scripts/generate-adapters.js +80 -80
- package/scripts/generate-from-template.js +97 -97
- package/scripts/generate-openapi.js +75 -75
- package/scripts/github-team-sync.js +80 -80
- package/scripts/install-hooks.js +20 -20
- package/scripts/load-plugins.js +65 -65
- package/scripts/migrate-openspec.js +318 -0
- package/scripts/migrate-speckit.js +322 -0
- package/scripts/migrate.js +12 -62
- package/scripts/onboard.js +312 -0
- package/scripts/pre-commit.js +56 -20
- package/scripts/team.js +113 -113
- package/scripts/test-adapters.js +118 -118
- package/scripts/test-create.js +13 -13
- package/scripts/test-end-to-end.js +137 -137
- package/scripts/test-router.js +110 -110
- package/scripts/test-state-transitions.js +146 -146
- package/scripts/test-validator.js +152 -152
- package/scripts/validate-config.js +36 -0
- package/scripts/validate-governance.js +150 -130
- package/scripts/verify.js +525 -0
- package/scripts/version-new.js +202 -0
- package/src/index.js +1010 -807
- package/templates/expo/plan.json +12 -0
- package/templates/expo/spec.json +12 -0
- package/templates/expo/tasks.json +5 -0
- package/templates/fastapi/plan.json +12 -0
- package/templates/fastapi/spec.json +12 -0
- package/templates/fastapi/tasks.json +5 -0
- package/templates/generic/plan.json +12 -0
- package/templates/generic/spec.json +11 -0
- package/templates/generic/tasks.json +5 -0
- package/templates/nextjs/plan.json +23 -0
- package/templates/nextjs/spec.json +12 -0
- package/templates/nextjs/tasks.json +5 -0
- package/templates/react-node/plan.json +15 -0
- package/templates/react-node/spec.json +12 -0
- package/templates/react-node/tasks.json +5 -0
- package/templates/registry.json +30 -0
- package/templates/turborepo/plan.json +12 -0
- package/templates/turborepo/spec.json +12 -0
- package/templates/turborepo/tasks.json +5 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
|
|
8
|
+
// ---------- Phase 1: Plan Reader ----------
|
|
9
|
+
|
|
10
|
+
function loadPlan(featureName) {
|
|
11
|
+
const planPath = path.join(projectRoot, ".sdrive", "governance", featureName, "plan.json");
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(planPath)) {
|
|
14
|
+
console.error(`❌ plan.json not found for feature "${featureName}".`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractPlanItems(plan) {
|
|
22
|
+
const items = [];
|
|
23
|
+
|
|
24
|
+
(plan.architecture?.components || []).forEach((c) => {
|
|
25
|
+
items.push({ type: "component", name: c.name, action: c.action });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
(plan.apiContracts || []).forEach((api) => {
|
|
29
|
+
items.push({ type: "endpoint", method: api.method, path: api.path, action: api.action });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
(plan.dataModel?.entities || []).forEach((entity) => {
|
|
33
|
+
items.push({ type: "entity", name: entity.name, action: entity.action });
|
|
34
|
+
(entity.fields || []).forEach((field) => {
|
|
35
|
+
items.push({
|
|
36
|
+
type: "field",
|
|
37
|
+
entity: entity.name,
|
|
38
|
+
name: field.name,
|
|
39
|
+
fieldType: field.type,
|
|
40
|
+
action: field.action
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
(plan.dependencies || []).forEach((dep) => {
|
|
46
|
+
items.push({ type: "dependency", name: dep.name, action: dep.action });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------- Phase 2: Code Scanner ----------
|
|
53
|
+
|
|
54
|
+
const CANDIDATE_ROOTS = [
|
|
55
|
+
"src",
|
|
56
|
+
"app",
|
|
57
|
+
"lib",
|
|
58
|
+
"pages",
|
|
59
|
+
"server",
|
|
60
|
+
"internal",
|
|
61
|
+
"cmd",
|
|
62
|
+
"pkg",
|
|
63
|
+
"api",
|
|
64
|
+
"apps",
|
|
65
|
+
"packages"
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const IGNORED_DIRS = new Set([
|
|
69
|
+
"node_modules",
|
|
70
|
+
".git",
|
|
71
|
+
"dist",
|
|
72
|
+
"build",
|
|
73
|
+
".next",
|
|
74
|
+
".expo",
|
|
75
|
+
".turbo",
|
|
76
|
+
"coverage",
|
|
77
|
+
"out",
|
|
78
|
+
".sdrive",
|
|
79
|
+
".cache",
|
|
80
|
+
"vendor"
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const CODE_EXTENSIONS = [
|
|
84
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
85
|
+
".py", ".go", ".rs", ".dart", ".java", ".kt",
|
|
86
|
+
".rb", ".php", ".cs", ".swift"
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
function readConstitutionSources() {
|
|
90
|
+
const constitutionPath = path.join(projectRoot, ".sdrive", "constitution.md");
|
|
91
|
+
|
|
92
|
+
if (!fs.existsSync(constitutionPath)) return [];
|
|
93
|
+
|
|
94
|
+
const content = fs.readFileSync(constitutionPath, "utf8");
|
|
95
|
+
const matches = content.match(/`([^`]+)\/`/g) || [];
|
|
96
|
+
|
|
97
|
+
return matches
|
|
98
|
+
.map((m) => m.replace(/`/g, "").replace(/\/$/, ""))
|
|
99
|
+
.filter((p) => CANDIDATE_ROOTS.includes(p));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function detectSourceRoots() {
|
|
103
|
+
const fromConstitution = readConstitutionSources();
|
|
104
|
+
|
|
105
|
+
if (fromConstitution.length > 0) {
|
|
106
|
+
return fromConstitution.filter((r) => fs.existsSync(path.join(projectRoot, r)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return CANDIDATE_ROOTS.filter((r) => fs.existsSync(path.join(projectRoot, r)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function walkDirectory(dir, files = []) {
|
|
113
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
114
|
+
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
117
|
+
|
|
118
|
+
const fullPath = path.join(dir, entry.name);
|
|
119
|
+
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
walkDirectory(fullPath, files);
|
|
122
|
+
} else if (CODE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
|
|
123
|
+
files.push(fullPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return files;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function scanCode() {
|
|
131
|
+
const roots = detectSourceRoots();
|
|
132
|
+
|
|
133
|
+
if (roots.length === 0) {
|
|
134
|
+
console.warn("⚠️ No source folders detected.");
|
|
135
|
+
return { files: [], roots: [] };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`🔍 Scanning roots: ${roots.join(", ")}\n`);
|
|
139
|
+
|
|
140
|
+
const files = [];
|
|
141
|
+
roots.forEach((root) => {
|
|
142
|
+
walkDirectory(path.join(projectRoot, root), files);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log(`📄 Found ${files.length} source files.\n`);
|
|
146
|
+
|
|
147
|
+
return { files, roots };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
// ---------- Phase 3: Diff Engine ----------
|
|
152
|
+
|
|
153
|
+
function readFileContent(filePath) {
|
|
154
|
+
try {
|
|
155
|
+
return fs.readFileSync(filePath, "utf8");
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function detectComponents(files) {
|
|
162
|
+
const components = [];
|
|
163
|
+
|
|
164
|
+
files.forEach((file) => {
|
|
165
|
+
const content = readFileContent(file);
|
|
166
|
+
const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
167
|
+
const basename = path.basename(file, path.extname(file));
|
|
168
|
+
|
|
169
|
+
// React / Vue / Svelte components
|
|
170
|
+
const componentMatches = content.match(
|
|
171
|
+
/(?:export\s+)?(?:function|const|class)\s+([A-Z][A-Za-z0-9_]+)/
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (componentMatches && /components?\//i.test(relative)) {
|
|
175
|
+
components.push({
|
|
176
|
+
name: componentMatches[1],
|
|
177
|
+
file: relative,
|
|
178
|
+
basename
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return components;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function detectEndpoints(files) {
|
|
187
|
+
const endpoints = [];
|
|
188
|
+
|
|
189
|
+
files.forEach((file) => {
|
|
190
|
+
const content = readFileContent(file);
|
|
191
|
+
const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
192
|
+
|
|
193
|
+
// Express / Fastify / Koa
|
|
194
|
+
const expressMatches = content.matchAll(
|
|
195
|
+
/(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([^'"`]+)['"`]/gi
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
for (const match of expressMatches) {
|
|
199
|
+
endpoints.push({
|
|
200
|
+
method: match[1].toUpperCase(),
|
|
201
|
+
path: match[2],
|
|
202
|
+
file: relative
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// FastAPI / Flask
|
|
207
|
+
const fastapiMatches = content.matchAll(
|
|
208
|
+
/@(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([^'"`]+)['"`]/gi
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
for (const match of fastapiMatches) {
|
|
212
|
+
endpoints.push({
|
|
213
|
+
method: match[1].toUpperCase(),
|
|
214
|
+
path: match[2],
|
|
215
|
+
file: relative
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Next.js API routes
|
|
220
|
+
if (/pages\/api\//.test(relative)) {
|
|
221
|
+
const route = relative.replace("pages/api", "").replace(/\.[jt]sx?$/, "");
|
|
222
|
+
endpoints.push({
|
|
223
|
+
method: "GET",
|
|
224
|
+
path: `/api${route}`,
|
|
225
|
+
file: relative
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return endpoints;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function detectEntities(files) {
|
|
234
|
+
const entities = [];
|
|
235
|
+
|
|
236
|
+
files.forEach((file) => {
|
|
237
|
+
const content = readFileContent(file);
|
|
238
|
+
const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
239
|
+
|
|
240
|
+
// SQL CREATE TABLE
|
|
241
|
+
const sqlMatches = content.matchAll(/CREATE TABLE\s+(\w+)/gi);
|
|
242
|
+
for (const match of sqlMatches) {
|
|
243
|
+
entities.push({
|
|
244
|
+
name: match[1],
|
|
245
|
+
file: relative
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Prisma models
|
|
250
|
+
const prismaMatches = content.matchAll(/model\s+(\w+)/g);
|
|
251
|
+
for (const match of prismaMatches) {
|
|
252
|
+
entities.push({
|
|
253
|
+
name: match[1],
|
|
254
|
+
file: relative
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// TypeORM entities
|
|
259
|
+
const typeormMatches = content.matchAll(/@Entity\([^)]*\)\s*class\s+(\w+)/g);
|
|
260
|
+
for (const match of typeormMatches) {
|
|
261
|
+
entities.push({
|
|
262
|
+
name: match[1],
|
|
263
|
+
file: relative
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return entities;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function detectFields(files) {
|
|
272
|
+
const fields = [];
|
|
273
|
+
|
|
274
|
+
files.forEach((file) => {
|
|
275
|
+
const content = readFileContent(file);
|
|
276
|
+
const relative = path.relative(projectRoot, file).replace(/\\/g, "/");
|
|
277
|
+
|
|
278
|
+
// SQL fields inside CREATE TABLE
|
|
279
|
+
const tableBlocks = content.matchAll(
|
|
280
|
+
/CREATE TABLE\s+(\w+)\s*\(([\s\S]*?)\)/gi
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
for (const block of tableBlocks) {
|
|
284
|
+
const entity = block[1];
|
|
285
|
+
const body = block[2];
|
|
286
|
+
|
|
287
|
+
const fieldMatches = body.matchAll(/^\s*(\w+)\s+\w+/gm);
|
|
288
|
+
|
|
289
|
+
for (const field of fieldMatches) {
|
|
290
|
+
fields.push({
|
|
291
|
+
entity,
|
|
292
|
+
name: field[1],
|
|
293
|
+
file: relative
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Prisma fields
|
|
299
|
+
const prismaBlocks = content.matchAll(/model\s+(\w+)\s*\{([\s\S]*?)\}/g);
|
|
300
|
+
|
|
301
|
+
for (const block of prismaBlocks) {
|
|
302
|
+
const entity = block[1];
|
|
303
|
+
const body = block[2];
|
|
304
|
+
|
|
305
|
+
const fieldMatches = body.matchAll(/^\s*(\w+)\s+\w+/gm);
|
|
306
|
+
|
|
307
|
+
for (const field of fieldMatches) {
|
|
308
|
+
fields.push({
|
|
309
|
+
entity,
|
|
310
|
+
name: field[1],
|
|
311
|
+
file: relative
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return fields;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function detectDependencies() {
|
|
321
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
322
|
+
|
|
323
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
327
|
+
return [
|
|
328
|
+
...Object.keys(pkg.dependencies || {}),
|
|
329
|
+
...Object.keys(pkg.devDependencies || {})
|
|
330
|
+
];
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function diffPlanAgainstCode(planItems, code) {
|
|
337
|
+
const matched = [];
|
|
338
|
+
const missing = [];
|
|
339
|
+
const renamed = [];
|
|
340
|
+
const extra = [];
|
|
341
|
+
|
|
342
|
+
const codeComponents = detectComponents(code.files);
|
|
343
|
+
const codeEndpoints = detectEndpoints(code.files);
|
|
344
|
+
const codeEntities = detectEntities(code.files);
|
|
345
|
+
const codeFields = detectFields(code.files);
|
|
346
|
+
const codeDependencies = detectDependencies();
|
|
347
|
+
|
|
348
|
+
planItems.forEach((item) => {
|
|
349
|
+
if (item.type === "component") {
|
|
350
|
+
const found = codeComponents.find((c) => c.name === item.name);
|
|
351
|
+
|
|
352
|
+
if (found) {
|
|
353
|
+
matched.push({ ...item, file: found.file });
|
|
354
|
+
} else {
|
|
355
|
+
// possible rename: same folder or similar name
|
|
356
|
+
const similar = codeComponents.find((c) =>
|
|
357
|
+
c.name.toLowerCase().includes(item.name.toLowerCase().slice(0, 5))
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
if (similar) {
|
|
361
|
+
renamed.push({ ...item, to: similar.name, file: similar.file });
|
|
362
|
+
} else {
|
|
363
|
+
missing.push(item);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (item.type === "endpoint") {
|
|
369
|
+
const found = codeEndpoints.find(
|
|
370
|
+
(e) => e.method === item.method && e.path === item.path
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (found) {
|
|
374
|
+
matched.push({ ...item, file: found.file });
|
|
375
|
+
} else {
|
|
376
|
+
missing.push(item);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (item.type === "entity") {
|
|
381
|
+
const found = codeEntities.find((e) => e.name === item.name);
|
|
382
|
+
|
|
383
|
+
if (found) {
|
|
384
|
+
matched.push({ ...item, file: found.file });
|
|
385
|
+
} else {
|
|
386
|
+
missing.push(item);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (item.type === "field") {
|
|
391
|
+
const found = codeFields.find(
|
|
392
|
+
(f) => f.entity === item.entity && f.name === item.name
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (found) {
|
|
396
|
+
matched.push({ ...item, file: found.file });
|
|
397
|
+
} else {
|
|
398
|
+
missing.push(item);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (item.type === "dependency") {
|
|
403
|
+
if (codeDependencies.includes(item.name)) {
|
|
404
|
+
matched.push(item);
|
|
405
|
+
} else {
|
|
406
|
+
missing.push(item);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Extra code detection
|
|
412
|
+
codeComponents.forEach((c) => {
|
|
413
|
+
if (!planItems.some((p) => p.type === "component" && p.name === c.name)) {
|
|
414
|
+
extra.push({ type: "component", name: c.name, file: c.file });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
codeEndpoints.forEach((e) => {
|
|
419
|
+
if (!planItems.some((p) => p.type === "endpoint" && p.path === e.path)) {
|
|
420
|
+
extra.push({ type: "endpoint", method: e.method, path: e.path, file: e.file });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return { matched, missing, renamed, extra };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
// ---------- Phase 4: Report Writer ----------
|
|
429
|
+
|
|
430
|
+
function determineStatus(result) {
|
|
431
|
+
if (result.missing.length > 0) return "fail";
|
|
432
|
+
if (result.renamed.length > 0) return "fail";
|
|
433
|
+
if (result.extra.length > 0) return "warn";
|
|
434
|
+
return "pass";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function writeReport(featureName, result) {
|
|
438
|
+
const status = determineStatus(result);
|
|
439
|
+
|
|
440
|
+
const report = {
|
|
441
|
+
feature: featureName,
|
|
442
|
+
status,
|
|
443
|
+
checkedAt: new Date().toISOString(),
|
|
444
|
+
summary: {
|
|
445
|
+
matched: result.matched.length,
|
|
446
|
+
missing: result.missing.length,
|
|
447
|
+
renamed: result.renamed.length,
|
|
448
|
+
extra: result.extra.length
|
|
449
|
+
},
|
|
450
|
+
items: {
|
|
451
|
+
matched: result.matched,
|
|
452
|
+
missing: result.missing,
|
|
453
|
+
renamed: result.renamed,
|
|
454
|
+
extra: result.extra
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const reportPath = path.join(
|
|
459
|
+
projectRoot,
|
|
460
|
+
".sdrive",
|
|
461
|
+
"governance",
|
|
462
|
+
featureName,
|
|
463
|
+
"verification.json"
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
467
|
+
|
|
468
|
+
return { status, reportPath };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
// ---------- Entry ----------
|
|
473
|
+
|
|
474
|
+
const featureName = process.argv[2];
|
|
475
|
+
|
|
476
|
+
if (!featureName) {
|
|
477
|
+
console.error("❌ Usage: node verify.js <feature-name>");
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const plan = loadPlan(featureName);
|
|
482
|
+
const planItems = extractPlanItems(plan);
|
|
483
|
+
const code = scanCode();
|
|
484
|
+
|
|
485
|
+
const result = diffPlanAgainstCode(planItems, code);
|
|
486
|
+
const { status, reportPath } = writeReport(featureName, result);
|
|
487
|
+
|
|
488
|
+
console.log("\n📊 Verification Result\n");
|
|
489
|
+
console.log(`✅ Matched: ${result.matched.length}`);
|
|
490
|
+
console.log(`❌ Missing: ${result.missing.length}`);
|
|
491
|
+
console.log(`⚠️ Renamed: ${result.renamed.length}`);
|
|
492
|
+
console.log(`➕ Extra: ${result.extra.length}`);
|
|
493
|
+
console.log(`\n🧾 Status: ${status.toUpperCase()}`);
|
|
494
|
+
console.log(`📁 Report: ${path.relative(projectRoot, reportPath)}\n`);
|
|
495
|
+
|
|
496
|
+
if (result.missing.length > 0) {
|
|
497
|
+
console.log("❌ Missing:");
|
|
498
|
+
result.missing.forEach((i) => {
|
|
499
|
+
console.log(` [${i.type}] ${i.name || i.path}`);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (result.renamed.length > 0) {
|
|
504
|
+
console.log("\n⚠️ Renamed:");
|
|
505
|
+
result.renamed.forEach((i) => {
|
|
506
|
+
console.log(` [${i.type}] ${i.name} → ${i.to}`);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (result.extra.length > 0) {
|
|
511
|
+
console.log("\n➕ Extra (not in plan):");
|
|
512
|
+
result.extra.forEach((i) => {
|
|
513
|
+
console.log(` [${i.type}] ${i.name || i.path}`);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
process.exit(status === "fail" ? 1 : 0);
|
|
518
|
+
|
|
519
|
+
module.exports = {
|
|
520
|
+
loadPlan,
|
|
521
|
+
extractPlanItems,
|
|
522
|
+
scanCode,
|
|
523
|
+
diffPlanAgainstCode,
|
|
524
|
+
writeReport
|
|
525
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const projectRoot = process.cwd();
|
|
7
|
+
|
|
8
|
+
const featureName = process.argv[2];
|
|
9
|
+
|
|
10
|
+
if (!featureName) {
|
|
11
|
+
console.error("❌ Usage: node version-new.js <feature>");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
16
|
+
const workflowStatePath = path.join(sdriveRoot, "workflow-state.json");
|
|
17
|
+
|
|
18
|
+
const specFeatureDir = findFeatureDir(path.join(sdriveRoot, "specs", "ongoing"), featureName);
|
|
19
|
+
const govFeatureDir = findFeatureDir(path.join(sdriveRoot, "governance"), featureName);
|
|
20
|
+
|
|
21
|
+
if (!specFeatureDir && !govFeatureDir) {
|
|
22
|
+
console.error(`❌ Feature "${featureName}" not found in ongoing or governance.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findFeatureDir(baseDir, name) {
|
|
27
|
+
if (!fs.existsSync(baseDir)) return null;
|
|
28
|
+
|
|
29
|
+
const candidate = path.join(baseDir, name);
|
|
30
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadWorkflowState() {
|
|
36
|
+
if (!fs.existsSync(workflowStatePath)) {
|
|
37
|
+
console.error("❌ workflow-state.json not found.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return JSON.parse(fs.readFileSync(workflowStatePath, "utf8"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function saveWorkflowState(state) {
|
|
44
|
+
fs.writeFileSync(workflowStatePath, JSON.stringify(state, null, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function computeNextVersion(feature) {
|
|
48
|
+
if (!feature.versions || feature.versions.length === 0) {
|
|
49
|
+
return "v1";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const maxVersion = feature.versions
|
|
53
|
+
.map((v) => parseInt(v.id.replace("v", ""), 10))
|
|
54
|
+
.reduce((a, b) => Math.max(a, b), 0);
|
|
55
|
+
|
|
56
|
+
return `v${maxVersion + 1}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function migrateToVersions(feature, specDir, govDir) {
|
|
60
|
+
console.log("🔧 Migrating feature to version folders...");
|
|
61
|
+
|
|
62
|
+
const v1SpecDir = path.join(specDir, "v1");
|
|
63
|
+
const v1GovDir = path.join(govDir, "v1");
|
|
64
|
+
|
|
65
|
+
fs.mkdirSync(v1SpecDir, { recursive: true });
|
|
66
|
+
fs.mkdirSync(v1GovDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
const specFiles = ["spec.md", "plan.md", "tasks.md"];
|
|
69
|
+
const govFiles = ["spec.json", "plan.json", "tasks.json", "traceability.json"];
|
|
70
|
+
|
|
71
|
+
specFiles.forEach((file) => {
|
|
72
|
+
const src = path.join(specDir, file);
|
|
73
|
+
const dest = path.join(v1SpecDir, file);
|
|
74
|
+
if (fs.existsSync(src)) {
|
|
75
|
+
fs.renameSync(src, dest);
|
|
76
|
+
console.log(` Moved: ${file} → v1/${file}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
govFiles.forEach((file) => {
|
|
81
|
+
const src = path.join(govDir, file);
|
|
82
|
+
const dest = path.join(v1GovDir, file);
|
|
83
|
+
if (fs.existsSync(src)) {
|
|
84
|
+
fs.renameSync(src, dest);
|
|
85
|
+
console.log(` Moved: ${file} → v1/${file}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
id: "v1",
|
|
91
|
+
phase: feature.phase || "ongoing",
|
|
92
|
+
currentStep: feature.currentStep || "specification",
|
|
93
|
+
gates: feature.gates || {
|
|
94
|
+
specification: "pending",
|
|
95
|
+
planning: "pending",
|
|
96
|
+
implementation: "pending",
|
|
97
|
+
review: "pending"
|
|
98
|
+
},
|
|
99
|
+
history: feature.history || []
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createVersionSkeleton(version, specDir, govDir) {
|
|
104
|
+
const versionSpecDir = path.join(specDir, version);
|
|
105
|
+
const versionGovDir = path.join(govDir, version);
|
|
106
|
+
|
|
107
|
+
fs.mkdirSync(versionSpecDir, { recursive: true });
|
|
108
|
+
fs.mkdirSync(versionGovDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
fs.writeFileSync(
|
|
111
|
+
path.join(versionSpecDir, "spec.md"),
|
|
112
|
+
`---\ntitle: ${featureName} (${version})\nstatus: backlog\ncreated: ${new Date().toISOString()}\nupdated: ${new Date().toISOString()}\n---\n\n# ${featureName} (${version})\n`
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
path.join(versionSpecDir, "plan.md"),
|
|
117
|
+
`# Technical Plan: ${featureName} (${version})\n`
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
path.join(versionSpecDir, "tasks.md"),
|
|
122
|
+
`# Execution Tasks: ${featureName} (${version})\n`
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(
|
|
126
|
+
path.join(versionGovDir, "spec.json"),
|
|
127
|
+
JSON.stringify({ feature: featureName, title: featureName, version: 1, overview: {}, userStories: [], functionalRequirements: [] }, null, 2)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(
|
|
131
|
+
path.join(versionGovDir, "plan.json"),
|
|
132
|
+
JSON.stringify({ feature: featureName, version: 1, architecture: {}, dataModel: {} }, null, 2)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
path.join(versionGovDir, "tasks.json"),
|
|
137
|
+
JSON.stringify({ feature: featureName, version: 1, phases: [] }, null, 2)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
fs.writeFileSync(
|
|
141
|
+
path.join(versionGovDir, "traceability.json"),
|
|
142
|
+
JSON.stringify({ feature: featureName, version: 1, rows: [] }, null, 2)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function main() {
|
|
147
|
+
const state = loadWorkflowState();
|
|
148
|
+
const feature = state.features?.find((f) => f.name === featureName);
|
|
149
|
+
|
|
150
|
+
if (!feature) {
|
|
151
|
+
console.error(`❌ Feature "${featureName}" not found in workflow-state.json.`);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Migrate if needed
|
|
156
|
+
if (!feature.versions || feature.versions.length === 0) {
|
|
157
|
+
const v1 = migrateToVersions(feature, specFeatureDir, govFeatureDir);
|
|
158
|
+
feature.versions = [v1];
|
|
159
|
+
feature.currentVersion = "v1";
|
|
160
|
+
|
|
161
|
+
// Remove old top-level fields
|
|
162
|
+
delete feature.phase;
|
|
163
|
+
delete feature.currentStep;
|
|
164
|
+
delete feature.gates;
|
|
165
|
+
delete feature.history;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const nextVersionId = computeNextVersion(feature);
|
|
169
|
+
|
|
170
|
+
console.log(`📦 Creating version ${nextVersionId} for "${featureName}"`);
|
|
171
|
+
|
|
172
|
+
createVersionSkeleton(nextVersionId, specFeatureDir, govFeatureDir);
|
|
173
|
+
|
|
174
|
+
feature.versions.push({
|
|
175
|
+
id: nextVersionId,
|
|
176
|
+
phase: "ongoing",
|
|
177
|
+
currentStep: "specification",
|
|
178
|
+
gates: {
|
|
179
|
+
specification: "pending",
|
|
180
|
+
planning: "pending",
|
|
181
|
+
implementation: "pending",
|
|
182
|
+
review: "pending"
|
|
183
|
+
},
|
|
184
|
+
history: [
|
|
185
|
+
{
|
|
186
|
+
event: "version_created",
|
|
187
|
+
timestamp: new Date().toISOString()
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
feature.currentVersion = nextVersionId;
|
|
193
|
+
|
|
194
|
+
saveWorkflowState(state);
|
|
195
|
+
|
|
196
|
+
console.log(`✅ Version ${nextVersionId} created.`);
|
|
197
|
+
console.log(` Spec folder: .sdrive/specs/ongoing/${featureName}/${nextVersionId}/`);
|
|
198
|
+
console.log(` Governance: .sdrive/governance/${featureName}/${nextVersionId}/`);
|
|
199
|
+
console.log(` Next: run /sdrive:propose to fill it.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
main();
|