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
package/dashboard/app.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
async function fetchJson(url) {
|
|
2
|
+
const res = await fetch(url);
|
|
3
|
+
return res.json();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function gateClass(status) {
|
|
7
|
+
if (status === "approved") return "gate approved";
|
|
8
|
+
if (status === "in_progress") return "gate in_progress";
|
|
9
|
+
return "gate pending";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getFilterValues() {
|
|
13
|
+
return {
|
|
14
|
+
search: document.getElementById("filter-search").value.trim().toLowerCase(),
|
|
15
|
+
phase: document.getElementById("filter-phase").value,
|
|
16
|
+
verification: document.getElementById("filter-verification").value,
|
|
17
|
+
tests: document.getElementById("filter-tests").value,
|
|
18
|
+
currentVersionOnly: document.getElementById("filter-current-version").checked
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function featureMatchesFilters(feature, version, filters, verification, tests) {
|
|
23
|
+
if (filters.search && !feature.name.toLowerCase().includes(filters.search)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (filters.phase && version.phase !== filters.phase) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (filters.verification) {
|
|
32
|
+
const status =
|
|
33
|
+
verification.status === "not_run" || !verification.status
|
|
34
|
+
? "not_run"
|
|
35
|
+
: verification.status;
|
|
36
|
+
|
|
37
|
+
if (status !== filters.verification) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (filters.tests) {
|
|
43
|
+
const testStatus =
|
|
44
|
+
tests.status === "not_run" || !tests.status ? "not_run" : tests.status;
|
|
45
|
+
|
|
46
|
+
if (testStatus !== filters.tests) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderVerification(verification) {
|
|
55
|
+
const div = document.createElement("div");
|
|
56
|
+
div.className = "traceability";
|
|
57
|
+
|
|
58
|
+
if (verification.status === "pass") {
|
|
59
|
+
div.innerHTML = `š Verification: <strong>PASS</strong> (${verification.summary?.matched ?? 0} matched)`;
|
|
60
|
+
div.style.color = "#2b6b25";
|
|
61
|
+
} else if (verification.status === "warn") {
|
|
62
|
+
div.innerHTML = `š Verification: <strong>WARN</strong> (${verification.summary?.extra ?? 0} extra, ${verification.summary?.matched ?? 0} matched)`;
|
|
63
|
+
div.style.color = "#7a5b1a";
|
|
64
|
+
} else if (verification.status === "fail") {
|
|
65
|
+
div.innerHTML = `š Verification: <strong>FAIL</strong> (${verification.summary?.missing ?? 0} missing, ${verification.summary?.renamed ?? 0} renamed)`;
|
|
66
|
+
div.style.color = "#8a1c1c";
|
|
67
|
+
} else {
|
|
68
|
+
div.innerHTML = `š Verification: <strong>NOT RUN</strong>`;
|
|
69
|
+
div.style.color = "#666";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return div;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderTests(tests) {
|
|
76
|
+
const div = document.createElement("div");
|
|
77
|
+
div.className = "traceability";
|
|
78
|
+
|
|
79
|
+
if (!tests || tests.status === "not_run") {
|
|
80
|
+
div.innerHTML = `š§Ŗ Tests: <strong>NOT RUN</strong>`;
|
|
81
|
+
div.style.color = "#666";
|
|
82
|
+
} else if (tests.status === "complete") {
|
|
83
|
+
div.innerHTML = `š§Ŗ Tests: <strong>${tests.summary.coverage}%</strong> (${tests.summary.testedACs}/${tests.summary.totalACs} ACs, ${tests.summary.passed}/${tests.summary.total} passed)`;
|
|
84
|
+
div.style.color = "#2b6b25";
|
|
85
|
+
} else {
|
|
86
|
+
div.innerHTML = `š§Ŗ Tests: <strong>${tests.summary.coverage}%</strong> (${tests.summary.testedACs}/${tests.summary.totalACs} ACs)`;
|
|
87
|
+
div.style.color = "#7a5b1a";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return div;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderVersion(feature, version, isCurrent) {
|
|
94
|
+
const div = document.createElement("div");
|
|
95
|
+
div.className = "feature-version";
|
|
96
|
+
div.style.marginTop = "12px";
|
|
97
|
+
div.style.paddingTop = "12px";
|
|
98
|
+
div.style.borderTop = "1px solid #eee";
|
|
99
|
+
|
|
100
|
+
div.innerHTML = `
|
|
101
|
+
<div style="font-weight: bold; margin-bottom: 4px;">
|
|
102
|
+
${isCurrent ? "š¢" : "āŖ"} Version: ${version.id}
|
|
103
|
+
</div>
|
|
104
|
+
<div>Phase: ${version.phase} | Step: ${version.currentStep}</div>
|
|
105
|
+
<div style="margin-top: 8px;">
|
|
106
|
+
<span class="${gateClass(version.gates?.specification)}">spec: ${version.gates?.specification}</span>
|
|
107
|
+
<span class="${gateClass(version.gates?.planning)}">plan: ${version.gates?.planning}</span>
|
|
108
|
+
<span class="${gateClass(version.gates?.implementation)}">impl: ${version.gates?.implementation}</span>
|
|
109
|
+
<span class="${gateClass(version.gates?.review)}">review: ${version.gates?.review}</span>
|
|
110
|
+
</div>
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
return div;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function renderFeatures() {
|
|
117
|
+
const state = await fetchJson("/api/workflow-state");
|
|
118
|
+
const container = document.getElementById("feature-list");
|
|
119
|
+
|
|
120
|
+
if (!state.features || state.features.length === 0) {
|
|
121
|
+
container.textContent = "No features yet.";
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const filters = getFilterValues();
|
|
126
|
+
container.innerHTML = "";
|
|
127
|
+
|
|
128
|
+
let rendered = 0;
|
|
129
|
+
|
|
130
|
+
for (const f of state.features) {
|
|
131
|
+
const featureDiv = document.createElement("div");
|
|
132
|
+
featureDiv.className = "feature";
|
|
133
|
+
|
|
134
|
+
featureDiv.innerHTML = `
|
|
135
|
+
<div class="feature-name">${f.name}</div>
|
|
136
|
+
${f.branch ? `<div class="feature-branch">šæ ${f.branch}</div>` : ""}
|
|
137
|
+
<div>Current Version: ${f.currentVersion}</div>
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
const versions = f.versions || [];
|
|
141
|
+
let versionRendered = 0;
|
|
142
|
+
|
|
143
|
+
for (const version of versions) {
|
|
144
|
+
const isCurrent = version.id === f.currentVersion;
|
|
145
|
+
|
|
146
|
+
if (filters.currentVersionOnly && !isCurrent) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const traceability = await fetchJson(`/api/traceability/${f.name}/${version.id}`);
|
|
151
|
+
const verification = await fetchJson(`/api/verification/${f.name}/${version.id}`);
|
|
152
|
+
const tests = await fetchJson(`/api/tests/${f.name}/${version.id}`);
|
|
153
|
+
|
|
154
|
+
if (!featureMatchesFilters(f, version, filters, verification, tests)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rows = traceability.rows || [];
|
|
159
|
+
const completed = rows.filter((r) => r.status === "complete").length;
|
|
160
|
+
const coverage = rows.length === 0 ? 0 : Math.round((completed / rows.length) * 100);
|
|
161
|
+
|
|
162
|
+
const versionDiv = renderVersion(f, version, isCurrent);
|
|
163
|
+
|
|
164
|
+
const traceLine = document.createElement("div");
|
|
165
|
+
traceLine.className = "traceability";
|
|
166
|
+
traceLine.textContent = `š Traceability Coverage: ${coverage}% (${completed}/${rows.length})`;
|
|
167
|
+
versionDiv.appendChild(traceLine);
|
|
168
|
+
|
|
169
|
+
versionDiv.appendChild(renderVerification(verification));
|
|
170
|
+
versionDiv.appendChild(renderTests(tests));
|
|
171
|
+
|
|
172
|
+
if (version.history) {
|
|
173
|
+
version.history.forEach((h) => {
|
|
174
|
+
if (h.event.endsWith("_approved")) {
|
|
175
|
+
const line = document.createElement("div");
|
|
176
|
+
line.className = "traceability";
|
|
177
|
+
line.textContent = `ā
${h.event.replace("_approved", "")} approved by ${h.by || "unknown"}`;
|
|
178
|
+
versionDiv.appendChild(line);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
featureDiv.appendChild(versionDiv);
|
|
184
|
+
versionRendered++;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (versionRendered > 0) {
|
|
188
|
+
container.appendChild(featureDiv);
|
|
189
|
+
rendered++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (rendered === 0) {
|
|
194
|
+
container.textContent = "No features match the current filters.";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function renderTeam() {
|
|
199
|
+
const team = await fetchJson("/api/team");
|
|
200
|
+
const container = document.getElementById("team-list");
|
|
201
|
+
|
|
202
|
+
if (!team.members || team.members.length === 0) {
|
|
203
|
+
container.textContent = "No team members.";
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
container.innerHTML = "";
|
|
208
|
+
team.members.forEach((m) => {
|
|
209
|
+
const div = document.createElement("div");
|
|
210
|
+
div.className = "member";
|
|
211
|
+
div.textContent = `${m.username} ā ${m.role}`;
|
|
212
|
+
container.appendChild(div);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function renderAudit() {
|
|
217
|
+
const log = await fetchJson("/api/audit-log");
|
|
218
|
+
const container = document.getElementById("audit-list");
|
|
219
|
+
|
|
220
|
+
if (!log.events || log.events.length === 0) {
|
|
221
|
+
container.textContent = "No activity yet.";
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
container.innerHTML = "";
|
|
226
|
+
log.events.slice(-20).reverse().forEach((e) => {
|
|
227
|
+
const div = document.createElement("div");
|
|
228
|
+
div.className = "audit-event";
|
|
229
|
+
div.textContent = `${e.timestamp} ā ${e.event}`;
|
|
230
|
+
container.appendChild(div);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renderExternalSystems(detected) {
|
|
235
|
+
if (!detected) return "";
|
|
236
|
+
|
|
237
|
+
const parts = [];
|
|
238
|
+
|
|
239
|
+
if (detected.openspec?.detected) {
|
|
240
|
+
parts.push(`
|
|
241
|
+
<li>
|
|
242
|
+
<strong>OpenSpec</strong> ā ${detected.openspec.specs || 0} specs,
|
|
243
|
+
${detected.openspec.changes || 0} active changes,
|
|
244
|
+
${detected.openspec.archived || 0} archived
|
|
245
|
+
</li>
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (detected.speckit?.detected) {
|
|
250
|
+
parts.push(`
|
|
251
|
+
<li>
|
|
252
|
+
<strong>Spec-Kit</strong> ā ${detected.speckit.specs || 0} specs,
|
|
253
|
+
constitution: ${detected.speckit.hasConstitution ? "found" : "missing"}
|
|
254
|
+
</li>
|
|
255
|
+
`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (parts.length === 0) return "";
|
|
259
|
+
|
|
260
|
+
return `
|
|
261
|
+
<div style="margin-top: 12px;">
|
|
262
|
+
<strong>š Detected External Spec Systems</strong>
|
|
263
|
+
<ul>${parts.join("")}</ul>
|
|
264
|
+
</div>
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderMigrated(migrated) {
|
|
269
|
+
if (!migrated || migrated.length === 0) return "";
|
|
270
|
+
|
|
271
|
+
const items = migrated.map((m) => {
|
|
272
|
+
const source = m.from === "openspec" ? "OpenSpec" : m.from === "speckit" ? "Spec-Kit" : m.from;
|
|
273
|
+
return `<li><strong>${m.feature}</strong> ā from ${source} (${m.source})</li>`;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return `
|
|
277
|
+
<div style="margin-top: 12px;">
|
|
278
|
+
<strong>š¦ Migrated Features</strong>
|
|
279
|
+
<ul>${items.join("")}</ul>
|
|
280
|
+
</div>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function renderSkipped(skipped) {
|
|
285
|
+
if (!skipped || skipped.length === 0) return "";
|
|
286
|
+
|
|
287
|
+
const items = skipped.map((s) => {
|
|
288
|
+
const system = s.system === "openspec" ? "OpenSpec" : s.system === "speckit" ? "Spec-Kit" : s.system;
|
|
289
|
+
const reason = s.reason ? ` ā ${s.reason}` : "";
|
|
290
|
+
return `<li><strong>${system}</strong>${reason}</li>`;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return `
|
|
294
|
+
<div style="margin-top: 12px;">
|
|
295
|
+
<strong>āļø Skipped Migrations</strong>
|
|
296
|
+
<ul>${items.join("")}</ul>
|
|
297
|
+
</div>
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function renderOnboarding(onboarding) {
|
|
302
|
+
const div = document.createElement("div");
|
|
303
|
+
div.className = "onboarding";
|
|
304
|
+
|
|
305
|
+
if (!onboarding) {
|
|
306
|
+
div.innerHTML = `
|
|
307
|
+
<h2>š¢ Greenfield Project</h2>
|
|
308
|
+
<div>Fully SpecDrive-native. No onboarding needed.</div>
|
|
309
|
+
`;
|
|
310
|
+
return div;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const isPartial = onboarding.missing && onboarding.missing.length > 0;
|
|
314
|
+
const isDeferred = onboarding.deferred && onboarding.deferred.length > 0;
|
|
315
|
+
|
|
316
|
+
div.innerHTML = `
|
|
317
|
+
<h2>${isPartial ? "š” Brownfield Project ā Partial Onboarding" : "š¢ Brownfield Project ā Onboarded"}</h2>
|
|
318
|
+
<div>Onboarded: ${new Date(onboarding.date).toLocaleDateString()}</div>
|
|
319
|
+
|
|
320
|
+
${renderExternalSystems(onboarding.detectedSystems)}
|
|
321
|
+
|
|
322
|
+
${
|
|
323
|
+
onboarding.missing && onboarding.missing.length
|
|
324
|
+
? `
|
|
325
|
+
<div style="margin-top: 12px;">
|
|
326
|
+
<strong>ā Missing</strong>
|
|
327
|
+
<ul>${onboarding.missing.map((m) => `<li>${m}</li>`).join("")}</ul>
|
|
328
|
+
</div>
|
|
329
|
+
`
|
|
330
|
+
: ""
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
${
|
|
334
|
+
isDeferred
|
|
335
|
+
? `
|
|
336
|
+
<div style="margin-top: 12px;">
|
|
337
|
+
<strong>ā ļø Deferred</strong>
|
|
338
|
+
<ul>${onboarding.deferred.map((m) => `<li>${m}</li>`).join("")}</ul>
|
|
339
|
+
</div>
|
|
340
|
+
`
|
|
341
|
+
: ""
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
${renderMigrated(onboarding.migrated)}
|
|
345
|
+
${renderSkipped(onboarding.skipped)}
|
|
346
|
+
|
|
347
|
+
<div style="margin-top: 12px;">
|
|
348
|
+
<strong>š Coverage</strong>
|
|
349
|
+
<ul>
|
|
350
|
+
<li>Retroactive features: ${onboarding.retroactiveFeatures?.length || 0}</li>
|
|
351
|
+
<li>Verified features: ${onboarding.verifiedFeatures?.length || 0}</li>
|
|
352
|
+
</ul>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
${
|
|
356
|
+
onboarding.notes
|
|
357
|
+
? `<div style="margin-top: 12px;"><em>${onboarding.notes}</em></div>`
|
|
358
|
+
: ""
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
return div;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function main() {
|
|
366
|
+
const onboarding = await fetchJson("/api/onboarding");
|
|
367
|
+
const onboardingContainer = document.getElementById("onboarding-panel");
|
|
368
|
+
|
|
369
|
+
if (onboardingContainer) {
|
|
370
|
+
onboardingContainer.innerHTML = "";
|
|
371
|
+
onboardingContainer.appendChild(renderOnboarding(onboarding));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Attach filter listeners
|
|
375
|
+
[
|
|
376
|
+
"filter-search",
|
|
377
|
+
"filter-phase",
|
|
378
|
+
"filter-verification",
|
|
379
|
+
"filter-tests",
|
|
380
|
+
"filter-current-version"
|
|
381
|
+
].forEach((id) => {
|
|
382
|
+
const el = document.getElementById(id);
|
|
383
|
+
if (el) {
|
|
384
|
+
el.addEventListener("input", renderFeatures);
|
|
385
|
+
el.addEventListener("change", renderFeatures);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await renderFeatures();
|
|
390
|
+
await renderTeam();
|
|
391
|
+
await renderAudit();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
main();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>SpecDrive Dashboard</title>
|
|
6
|
+
<link rel="stylesheet" href="/style.css" />
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<header>
|
|
10
|
+
<h1>SpecDrive Dashboard</h1>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<main>
|
|
14
|
+
<section id="onboarding">
|
|
15
|
+
<div id="onboarding-panel">Loading...</div>
|
|
16
|
+
</section>
|
|
17
|
+
|
|
18
|
+
<section id="features">
|
|
19
|
+
<h2>Features</h2>
|
|
20
|
+
|
|
21
|
+
<div id="feature-filters" style="margin-bottom: 16px;">
|
|
22
|
+
<input
|
|
23
|
+
id="filter-search"
|
|
24
|
+
type="text"
|
|
25
|
+
placeholder="Search features..."
|
|
26
|
+
style="padding: 6px 10px; font-size: 13px; margin-right: 8px; border: 1px solid #ddd; border-radius: 4px;"
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
<select id="filter-phase" style="padding: 6px 10px; font-size: 13px; margin-right: 8px; border: 1px solid #ddd; border-radius: 4px;">
|
|
30
|
+
<option value="">All phases</option>
|
|
31
|
+
<option value="backlog">backlog</option>
|
|
32
|
+
<option value="ongoing">ongoing</option>
|
|
33
|
+
<option value="completed">completed</option>
|
|
34
|
+
<option value="blocked">blocked</option>
|
|
35
|
+
</select>
|
|
36
|
+
|
|
37
|
+
<select id="filter-verification" style="padding: 6px 10px; font-size: 13px; margin-right: 8px; border: 1px solid #ddd; border-radius: 4px;">
|
|
38
|
+
<option value="">All verification</option>
|
|
39
|
+
<option value="pass">pass</option>
|
|
40
|
+
<option value="warn">warn</option>
|
|
41
|
+
<option value="fail">fail</option>
|
|
42
|
+
<option value="not_run">not run</option>
|
|
43
|
+
</select>
|
|
44
|
+
|
|
45
|
+
<select id="filter-tests" style="padding: 6px 10px; font-size: 13px; margin-right: 8px; border: 1px solid #ddd; border-radius: 4px;">
|
|
46
|
+
<option value="">All test coverage</option>
|
|
47
|
+
<option value="complete">complete (100%)</option>
|
|
48
|
+
<option value="partial">partial</option>
|
|
49
|
+
<option value="not_run">not run</option>
|
|
50
|
+
</select>
|
|
51
|
+
|
|
52
|
+
<label style="font-size: 13px; margin-right: 8px;">
|
|
53
|
+
<input id="filter-current-version" type="checkbox" />
|
|
54
|
+
Current version only
|
|
55
|
+
</label>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div id="feature-list">Loading...</div>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
<section id="team">
|
|
62
|
+
<h2>Team</h2>
|
|
63
|
+
<div id="team-list">Loading...</div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<section id="audit">
|
|
67
|
+
<h2>Recent Activity</h2>
|
|
68
|
+
<div id="audit-list">Loading...</div>
|
|
69
|
+
</section>
|
|
70
|
+
</main>
|
|
71
|
+
|
|
72
|
+
<script src="/app.js"></script>
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const projectRoot = process.cwd();
|
|
8
|
+
const sdriveRoot = path.join(projectRoot, ".sdrive");
|
|
9
|
+
|
|
10
|
+
function loadJson(filePath, fallback = null) {
|
|
11
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function apiWorkflowState() {
|
|
20
|
+
return loadJson(path.join(sdriveRoot, "workflow-state.json"), { features: [] });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function apiAuditLog() {
|
|
24
|
+
return loadJson(path.join(sdriveRoot, "audit-log.json"), { events: [] });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function apiTeam() {
|
|
28
|
+
return loadJson(path.join(sdriveRoot, "team.json"), { members: [] });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function apiTraceability(feature, version) {
|
|
32
|
+
const tracePath = path.join(sdriveRoot, "governance", feature, version, "traceability.json");
|
|
33
|
+
return loadJson(tracePath, { rows: [] });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function apiVerification(feature, version) {
|
|
37
|
+
const verificationPath = path.join(sdriveRoot, "governance", feature, version, "verification.json");
|
|
38
|
+
return loadJson(verificationPath, { status: "not_run", summary: {}, items: {} });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function apiTestReport(feature, version) {
|
|
42
|
+
const reportDir = path.join(sdriveRoot, "reports", "tests");
|
|
43
|
+
|
|
44
|
+
if (!fs.existsSync(reportDir)) {
|
|
45
|
+
return { status: "not_run", summary: {}, tests: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const files = fs.readdirSync(reportDir)
|
|
49
|
+
.filter((f) => f.startsWith(`test-report-${feature}-${version}-`))
|
|
50
|
+
.sort()
|
|
51
|
+
.reverse();
|
|
52
|
+
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
return { status: "not_run", summary: {}, tests: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const latest = path.join(reportDir, files[0]);
|
|
58
|
+
const content = fs.readFileSync(latest, "utf8");
|
|
59
|
+
|
|
60
|
+
const totalMatch = content.match(/Total ACs:\*\*\s*(\d+)/);
|
|
61
|
+
const testedMatch = content.match(/Tested ACs:\*\*\s*(\d+)/);
|
|
62
|
+
const coverageMatch = content.match(/Coverage:\*\*\s*(\d+)/);
|
|
63
|
+
const passedMatch = content.match(/Tests Passed:\*\*\s*(\d+)\/(\d+)/);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
status:
|
|
67
|
+
testedMatch && totalMatch && testedMatch[1] === totalMatch[1]
|
|
68
|
+
? "complete"
|
|
69
|
+
: "partial",
|
|
70
|
+
file: files[0],
|
|
71
|
+
summary: {
|
|
72
|
+
totalACs: totalMatch ? parseInt(totalMatch[1]) : 0,
|
|
73
|
+
testedACs: testedMatch ? parseInt(testedMatch[1]) : 0,
|
|
74
|
+
coverage: coverageMatch ? parseInt(coverageMatch[1]) : 0,
|
|
75
|
+
passed: passedMatch ? parseInt(passedMatch[1]) : 0,
|
|
76
|
+
total: passedMatch ? parseInt(passedMatch[2]) : 0
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseFeatureVersion(url) {
|
|
82
|
+
const parts = url.split("/").filter(Boolean);
|
|
83
|
+
return {
|
|
84
|
+
feature: parts[parts.length - 2],
|
|
85
|
+
version: parts[parts.length - 1]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function serveStatic(res, filePath, contentType) {
|
|
90
|
+
if (!fs.existsSync(filePath)) {
|
|
91
|
+
res.writeHead(404);
|
|
92
|
+
res.end("Not found");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
97
|
+
res.end(fs.readFileSync(filePath));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function apiOnboarding() {
|
|
101
|
+
const state = loadJson(path.join(sdriveRoot, "workflow-state.json"), { features: [] });
|
|
102
|
+
return state.onboarding || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const server = http.createServer((req, res) => {
|
|
106
|
+
const url = req.url;
|
|
107
|
+
|
|
108
|
+
if (url.startsWith("/api/")) {
|
|
109
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
110
|
+
|
|
111
|
+
if (url === "/api/workflow-state") {
|
|
112
|
+
return res.end(JSON.stringify(apiWorkflowState()));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (url === "/api/audit-log") {
|
|
116
|
+
return res.end(JSON.stringify(apiAuditLog()));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (url === "/api/team") {
|
|
120
|
+
return res.end(JSON.stringify(apiTeam()));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (url.startsWith("/api/traceability/")) {
|
|
124
|
+
const { feature, version } = parseFeatureVersion(url);
|
|
125
|
+
return res.end(JSON.stringify(apiTraceability(feature, version)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (url.startsWith("/api/verification/")) {
|
|
129
|
+
const { feature, version } = parseFeatureVersion(url);
|
|
130
|
+
return res.end(JSON.stringify(apiVerification(feature, version)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (url.startsWith("/api/tests/")) {
|
|
134
|
+
const { feature, version } = parseFeatureVersion(url);
|
|
135
|
+
return res.end(JSON.stringify(apiTestReport(feature, version)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (url === "/api/onboarding") {
|
|
139
|
+
return res.end(JSON.stringify(apiOnboarding()));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
res.writeHead(404);
|
|
143
|
+
return res.end(JSON.stringify({ error: "Not found" }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (url === "/" || url === "/index.html") {
|
|
147
|
+
return serveStatic(res, path.join(__dirname, "index.html"), "text/html");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (url === "/style.css") {
|
|
151
|
+
return serveStatic(res, path.join(__dirname, "style.css"), "text/css");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (url === "/app.js") {
|
|
155
|
+
return serveStatic(res, path.join(__dirname, "app.js"), "application/javascript");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
res.writeHead(404);
|
|
159
|
+
res.end("Not found");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const PORT = 4747;
|
|
163
|
+
|
|
164
|
+
server.listen(PORT, () => {
|
|
165
|
+
console.log(`\nš SpecDrive Dashboard running at http://localhost:${PORT}\n`);
|
|
166
|
+
});
|