ai-spec-dev 0.55.0 → 0.57.0
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 +5 -1
- package/cli/pipeline/multi-repo.ts +9 -0
- package/core/cross-stack-verifier.ts +90 -3
- package/demo/demo.gif +0 -0
- package/demo/demo.sh +433 -0
- package/demo/demo.tape +52 -0
- package/dist/cli/index.js +68 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +68 -5
- package/dist/cli/index.mjs.map +1 -1
- package/docs-assets/demo.gif +0 -0
- package/package.json +1 -1
- package/specs/2026-04-08-vision-frontend-workflow-design.md +548 -0
- package/tests/cross-stack-verifier.test.ts +101 -0
- package/.ai-spec-workspace.json +0 -17
- package/.ai-spec.json +0 -7
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<a href="https://github.com/hzhongzhong/ai-spec"><img src="https://img.shields.io/badge/GitHub-ai--spec-181717?logo=github" alt="GitHub" /></a>
|
|
14
14
|
<a href="https://www.npmjs.com/package/ai-spec-dev"><img src="https://img.shields.io/npm/v/ai-spec-dev?color=cb3837&logo=npm" alt="npm" /></a>
|
|
15
|
-
<img src="https://img.shields.io/badge/version-0.
|
|
15
|
+
<img src="https://img.shields.io/badge/version-0.57.0-blue" alt="version" />
|
|
16
16
|
<img src="https://img.shields.io/badge/tests-913%20passed-brightgreen" alt="tests" />
|
|
17
17
|
<img src="https://img.shields.io/badge/providers-9-orange" alt="providers" />
|
|
18
18
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
<a href="#english">English</a> | <a href="#中文文档">中文</a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="docs-assets/demo.gif" alt="ai-spec demo" width="860" />
|
|
27
|
+
</p>
|
|
28
|
+
|
|
25
29
|
---
|
|
26
30
|
|
|
27
31
|
<h2 id="english">English</h2>
|
|
@@ -662,6 +662,15 @@ export async function runMultiRepoPipeline(
|
|
|
662
662
|
{ scopedFiles: fe.generatedFiles }
|
|
663
663
|
);
|
|
664
664
|
printCrossStackReport(fe.repoName, report);
|
|
665
|
+
if (report.hasViolations) {
|
|
666
|
+
console.log(
|
|
667
|
+
chalk.yellow(
|
|
668
|
+
` ⚠ [W5] ${fe.repoName} has cross-stack violations` +
|
|
669
|
+
` (${report.phantom.length} phantom, ${report.methodMismatch.length} method mismatch).` +
|
|
670
|
+
` Review the report above and fix generated frontend code.`
|
|
671
|
+
)
|
|
672
|
+
);
|
|
673
|
+
}
|
|
665
674
|
} catch (err) {
|
|
666
675
|
console.log(chalk.yellow(` ⚠ Verification failed for ${fe.repoName}: ${(err as Error).message}`));
|
|
667
676
|
}
|
|
@@ -11,6 +11,9 @@ export interface FrontendApiCall {
|
|
|
11
11
|
file: string; // relative path from frontend root
|
|
12
12
|
line: number; // 1-indexed line number
|
|
13
13
|
snippet: string; // one-line source snippet
|
|
14
|
+
/** True when path was extracted from a string concatenation (e.g. '/api/' + id).
|
|
15
|
+
* The path ends with /* to represent the unknown suffix — matching is approximate. */
|
|
16
|
+
isConcatPath?: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface CrossStackReport {
|
|
@@ -24,7 +27,12 @@ export interface CrossStackReport {
|
|
|
24
27
|
methodMismatch: Array<{ call: FrontendApiCall; expectedMethod: string }>;
|
|
25
28
|
/** Calls whose method+path both match the DSL */
|
|
26
29
|
matched: Array<{ call: FrontendApiCall; endpointId: string }>;
|
|
30
|
+
/** Calls with UNKNOWN method (generic `request('/path')` helpers without a method arg).
|
|
31
|
+
* These are counted as matched (permissive) but surfaced for visibility. */
|
|
32
|
+
unknownMethodCalls: FrontendApiCall[];
|
|
27
33
|
totalScannedFiles: number;
|
|
34
|
+
/** True when there are phantom calls or method mismatches — use to fail CI / pipeline steps. */
|
|
35
|
+
hasViolations: boolean;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
// ─── File scanning ────────────────────────────────────────────────────────────
|
|
@@ -92,8 +100,9 @@ export function extractApiCallsFromSource(
|
|
|
92
100
|
|
|
93
101
|
// Pattern 1: .get('/path') / .post('/path') / .delete('/path') / .put('/path') / .patch('/path')
|
|
94
102
|
// Matches things like: axios.get('/api/users'), api.post(`/api/users/${id}`)
|
|
103
|
+
// Negative lookahead (?!\s*\+) ensures we don't match string concatenation (handled by Pattern 5).
|
|
95
104
|
const methodCallRegex =
|
|
96
|
-
/\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2/gi;
|
|
105
|
+
/\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2(?!\s*\+)/gi;
|
|
97
106
|
|
|
98
107
|
// Pattern 2: fetch('/path', { method: 'POST' })
|
|
99
108
|
// We detect fetch( + URL + optional method in the next ~100 chars
|
|
@@ -107,6 +116,15 @@ export function extractApiCallsFromSource(
|
|
|
107
116
|
const genericRequestRegex =
|
|
108
117
|
/\brequest\s*\(\s*(['"`])([^'"`]+)\1\s*(?:,\s*(['"`])(GET|POST|PUT|PATCH|DELETE)\3)?/gi;
|
|
109
118
|
|
|
119
|
+
// Pattern 5: axios.get('/api/prefix/' + variable) — string concatenation with static prefix.
|
|
120
|
+
// We capture the static prefix and treat the unknown suffix as a wildcard segment.
|
|
121
|
+
// Only the method-call variant is handled here; fetch+concat is covered separately below.
|
|
122
|
+
const concatMethodRegex =
|
|
123
|
+
/\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2\s*\+/gi;
|
|
124
|
+
|
|
125
|
+
// Pattern 6: fetch('/api/prefix/' + variable, ...) — concat inside fetch
|
|
126
|
+
const concatFetchRegex = /\bfetch\s*\(\s*(['"`])([^'"`]+)\1\s*\+([^)]*)\)/g;
|
|
127
|
+
|
|
110
128
|
function getLineNumber(offset: number): number {
|
|
111
129
|
// Count newlines up to offset
|
|
112
130
|
let ln = 1;
|
|
@@ -131,6 +149,15 @@ export function extractApiCallsFromSource(
|
|
|
131
149
|
return true;
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
/** Build a wildcard-terminated path from a static concat prefix.
|
|
153
|
+
* '/api/users/' → '/api/users/*'
|
|
154
|
+
* '/api/users' → '/api/users/*'
|
|
155
|
+
*/
|
|
156
|
+
function concatPath(prefix: string): string {
|
|
157
|
+
const stripped = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
158
|
+
return stripped + "/*";
|
|
159
|
+
}
|
|
160
|
+
|
|
134
161
|
let match: RegExpExecArray | null;
|
|
135
162
|
|
|
136
163
|
while ((match = methodCallRegex.exec(source)) !== null) {
|
|
@@ -189,6 +216,39 @@ export function extractApiCallsFromSource(
|
|
|
189
216
|
});
|
|
190
217
|
}
|
|
191
218
|
|
|
219
|
+
// Pattern 5: axios.get('/api/prefix/' + variable)
|
|
220
|
+
// Pattern 1's negative lookahead excludes these cases, so no dedup needed.
|
|
221
|
+
while ((match = concatMethodRegex.exec(source)) !== null) {
|
|
222
|
+
const rawPrefix = match[3];
|
|
223
|
+
if (!isApiLike(rawPrefix)) continue;
|
|
224
|
+
const line = getLineNumber(match.index);
|
|
225
|
+
calls.push({
|
|
226
|
+
method: match[1].toUpperCase(),
|
|
227
|
+
path: concatPath(rawPrefix),
|
|
228
|
+
file: relFile,
|
|
229
|
+
line,
|
|
230
|
+
snippet: getSnippet(line),
|
|
231
|
+
isConcatPath: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Pattern 6: fetch('/api/prefix/' + variable, ...)
|
|
236
|
+
while ((match = concatFetchRegex.exec(source)) !== null) {
|
|
237
|
+
const rawPrefix = match[2];
|
|
238
|
+
if (!isApiLike(rawPrefix)) continue;
|
|
239
|
+
const tail = match[3] ?? "";
|
|
240
|
+
const methodMatch = tail.match(/method\s*:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/i);
|
|
241
|
+
const line = getLineNumber(match.index);
|
|
242
|
+
calls.push({
|
|
243
|
+
method: methodMatch ? methodMatch[1].toUpperCase() : "GET",
|
|
244
|
+
path: concatPath(rawPrefix),
|
|
245
|
+
file: relFile,
|
|
246
|
+
line,
|
|
247
|
+
snippet: getSnippet(line),
|
|
248
|
+
isConcatPath: true,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
192
252
|
return calls;
|
|
193
253
|
}
|
|
194
254
|
|
|
@@ -211,6 +271,7 @@ export function normalizePathSegments(p: string): string[] {
|
|
|
211
271
|
const withoutQs = p.split("?")[0];
|
|
212
272
|
const segments = withoutQs.split("/").filter(Boolean);
|
|
213
273
|
return segments.map((seg) => {
|
|
274
|
+
if (seg === "*") return "*"; // explicit wildcard (concat paths)
|
|
214
275
|
if (seg.startsWith(":")) return "*";
|
|
215
276
|
if (seg.includes("${") || seg.includes("{{")) return "*";
|
|
216
277
|
if (/^\d+$/.test(seg)) return "*";
|
|
@@ -286,9 +347,13 @@ export async function verifyCrossStackContract(
|
|
|
286
347
|
const phantom: FrontendApiCall[] = [];
|
|
287
348
|
const methodMismatch: Array<{ call: FrontendApiCall; expectedMethod: string }> = [];
|
|
288
349
|
const matched: Array<{ call: FrontendApiCall; endpointId: string }> = [];
|
|
350
|
+
const unknownMethodCalls: FrontendApiCall[] = [];
|
|
289
351
|
const usedEndpointIds = new Set<string>();
|
|
290
352
|
|
|
291
353
|
for (const call of allCalls) {
|
|
354
|
+
// Track UNKNOWN-method calls for visibility regardless of matching outcome.
|
|
355
|
+
if (call.method === "UNKNOWN") unknownMethodCalls.push(call);
|
|
356
|
+
|
|
292
357
|
// Find all DSL endpoints whose path matches this call's path.
|
|
293
358
|
const pathMatches = backendEndpoints.filter((ep) => pathsMatch(ep.path, call.path));
|
|
294
359
|
if (pathMatches.length === 0) {
|
|
@@ -296,6 +361,7 @@ export async function verifyCrossStackContract(
|
|
|
296
361
|
continue;
|
|
297
362
|
}
|
|
298
363
|
// Check if any path-match also matches the method.
|
|
364
|
+
// UNKNOWN is treated permissively — matched against the first path hit.
|
|
299
365
|
const methodMatch = pathMatches.find(
|
|
300
366
|
(ep) => call.method === "UNKNOWN" || ep.method === call.method
|
|
301
367
|
);
|
|
@@ -319,7 +385,9 @@ export async function verifyCrossStackContract(
|
|
|
319
385
|
unused,
|
|
320
386
|
methodMismatch,
|
|
321
387
|
matched,
|
|
388
|
+
unknownMethodCalls,
|
|
322
389
|
totalScannedFiles: files.length,
|
|
390
|
+
hasViolations: phantom.length > 0 || methodMismatch.length > 0,
|
|
323
391
|
};
|
|
324
392
|
}
|
|
325
393
|
|
|
@@ -332,10 +400,12 @@ export function printCrossStackReport(repoName: string, report: CrossStackReport
|
|
|
332
400
|
const mismatchCount = report.methodMismatch.length;
|
|
333
401
|
const unusedCount = report.unused.length;
|
|
334
402
|
|
|
403
|
+
const concatCount = report.frontendCalls.filter((c) => c.isConcatPath).length;
|
|
404
|
+
const concatNote = concatCount > 0 ? ` (${concatCount} via string concat — approximate)` : "";
|
|
335
405
|
console.log(chalk.cyan(`\n─── Cross-Stack Contract Verification [${repoName}] ─────────────`));
|
|
336
406
|
console.log(
|
|
337
407
|
chalk.gray(
|
|
338
|
-
` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)`
|
|
408
|
+
` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)${concatNote}`
|
|
339
409
|
)
|
|
340
410
|
);
|
|
341
411
|
console.log(chalk.gray(` Backend DSL endpoints: ${totalEp}`));
|
|
@@ -387,8 +457,25 @@ export function printCrossStackReport(repoName: string, report: CrossStackReport
|
|
|
387
457
|
}
|
|
388
458
|
}
|
|
389
459
|
|
|
460
|
+
// ── UNKNOWN method calls ─────────────────────────────────────────────────────
|
|
461
|
+
// Surface for visibility; they were matched permissively and may hide real mismatches.
|
|
462
|
+
if (report.unknownMethodCalls.length > 0) {
|
|
463
|
+
console.log(
|
|
464
|
+
chalk.gray(
|
|
465
|
+
`\n · Unknown method (${report.unknownMethodCalls.length}): HTTP method could not be determined — matched permissively`
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
for (const call of report.unknownMethodCalls.slice(0, 5)) {
|
|
469
|
+
console.log(chalk.gray(` UNKNWN ${call.path}`));
|
|
470
|
+
console.log(chalk.gray(` ${call.file}:${call.line}`));
|
|
471
|
+
}
|
|
472
|
+
if (report.unknownMethodCalls.length > 5) {
|
|
473
|
+
console.log(chalk.gray(` ... and ${report.unknownMethodCalls.length - 5} more`));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
390
477
|
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
391
|
-
if (
|
|
478
|
+
if (!report.hasViolations && unusedCount === 0 && matchedCount === totalEp && totalEp > 0) {
|
|
392
479
|
console.log(chalk.green(`\n ✔ Contract fully aligned — all ${totalEp} endpoints consumed correctly.`));
|
|
393
480
|
}
|
|
394
481
|
console.log(chalk.cyan("─".repeat(65)));
|
package/demo/demo.gif
ADDED
|
Binary file
|
package/demo/demo.sh
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ai-spec demo simulation script
|
|
3
|
+
# Usage: bash demo.sh [scene]
|
|
4
|
+
# scene: help | create | multirepo | artifacts | observability | all
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# ── Colors ────────────────────────────────────────────────────────────────────
|
|
9
|
+
RESET='\033[0m'
|
|
10
|
+
BOLD='\033[1m'
|
|
11
|
+
DIM='\033[2m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
BGREEN='\033[1;32m'
|
|
14
|
+
CYAN='\033[0;36m'
|
|
15
|
+
BCYAN='\033[1;36m'
|
|
16
|
+
YELLOW='\033[0;33m'
|
|
17
|
+
BYELLOW='\033[1;33m'
|
|
18
|
+
BLUE='\033[0;34m'
|
|
19
|
+
BBLUE='\033[1;34m'
|
|
20
|
+
MAGENTA='\033[0;35m'
|
|
21
|
+
BMAGENTA='\033[1;35m'
|
|
22
|
+
RED='\033[0;31m'
|
|
23
|
+
WHITE='\033[1;37m'
|
|
24
|
+
GRAY='\033[0;90m'
|
|
25
|
+
|
|
26
|
+
p() { printf "%b\n" "$*"; }
|
|
27
|
+
pp() { printf "%b" "$*"; }
|
|
28
|
+
nl() { echo ""; }
|
|
29
|
+
pause() { sleep "${1:-0.6}"; }
|
|
30
|
+
slow_pause() { sleep "${1:-1.2}"; }
|
|
31
|
+
|
|
32
|
+
bar_full() { pp "${BGREEN}████████████████████${RESET}"; }
|
|
33
|
+
bar_part() { pp "${BGREEN}████████████${RESET}${GRAY}████████${RESET}"; }
|
|
34
|
+
bar_start() { pp "${BGREEN}████${RESET}${GRAY}████████████████${RESET}"; }
|
|
35
|
+
|
|
36
|
+
score_bar() {
|
|
37
|
+
local score=$1 max=10
|
|
38
|
+
local filled=$(( score * 2 ))
|
|
39
|
+
local empty=$(( (max - score) * 2 ))
|
|
40
|
+
pp "${BGREEN}"
|
|
41
|
+
for ((i=0; i<filled; i++)); do pp "█"; done
|
|
42
|
+
pp "${GRAY}"
|
|
43
|
+
for ((i=0; i<empty; i++)); do pp "░"; done
|
|
44
|
+
pp "${RESET}"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# ── Scene 1: help ─────────────────────────────────────────────────────────────
|
|
48
|
+
scene_help() {
|
|
49
|
+
node /Users/zuozhichao/Documents/ai-spec-dev-poc/dist/cli/index.js --help
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ── Scene 2: single-repo create pipeline ──────────────────────────────────────
|
|
53
|
+
scene_create() {
|
|
54
|
+
nl
|
|
55
|
+
p "${BCYAN}┌─────────────────────────────────────────────────┐${RESET}"
|
|
56
|
+
p "${BCYAN}│ ai-spec · Single-Repo Pipeline │${RESET}"
|
|
57
|
+
p "${BCYAN}└─────────────────────────────────────────────────┘${RESET}"
|
|
58
|
+
nl
|
|
59
|
+
pause 0.5
|
|
60
|
+
|
|
61
|
+
# Repo selection
|
|
62
|
+
p "${BOLD}[Repo]${RESET} Select repo(s) for this feature:"
|
|
63
|
+
pause 0.4
|
|
64
|
+
p " ${BGREEN}●${RESET} rushbuy-web-admin ${GRAY}(vue / frontend)${RESET}"
|
|
65
|
+
p " ${GRAY}○ rushbuy-node-service (node-express / backend)${RESET}"
|
|
66
|
+
pause 0.5
|
|
67
|
+
p " ${BGREEN}✔${RESET} ${BOLD}1 repo selected${RESET}"
|
|
68
|
+
nl
|
|
69
|
+
pause 0.6
|
|
70
|
+
|
|
71
|
+
# Step 1: Context
|
|
72
|
+
p "${BOLD}[1/10]${RESET} Loading project context..."
|
|
73
|
+
pause 0.5
|
|
74
|
+
p " Constitution : ${BGREEN}✔ found${RESET} ${GRAY}(.ai-spec-constitution.md §1–§9)${RESET}"
|
|
75
|
+
p " Tech stack : ${CYAN}vue · vite · pinia · axios${RESET}"
|
|
76
|
+
p " Routes found : ${CYAN}24${RESET}"
|
|
77
|
+
p " Stores found : ${CYAN}8${RESET}"
|
|
78
|
+
p " HTTP client : ${CYAN}import http from '@/utils/http'${RESET}"
|
|
79
|
+
nl
|
|
80
|
+
pause 0.7
|
|
81
|
+
|
|
82
|
+
# Step 1.5: Design Options Dialogue
|
|
83
|
+
p "${BOLD}[1.5/10]${RESET} ${MAGENTA}Design Options Dialogue${RESET}"
|
|
84
|
+
pause 0.4
|
|
85
|
+
p " AI proposes ${BOLD}3 architecture options${RESET}:"
|
|
86
|
+
pause 0.3
|
|
87
|
+
p " ${BOLD}A)${RESET} Kanban board view ${GRAY}— drag-and-drop, column per status${RESET}"
|
|
88
|
+
p " ${BOLD}B)${RESET} Table + filters ${GRAY}— sortable, bulk actions, pagination${RESET}"
|
|
89
|
+
p " ${BOLD}C)${RESET} Split-pane layout ${GRAY}— list left, detail right${RESET}"
|
|
90
|
+
pause 0.5
|
|
91
|
+
p " ${BGREEN}✔${RESET} Selected: ${BOLD}B — Table + filters${RESET}"
|
|
92
|
+
nl
|
|
93
|
+
pause 0.6
|
|
94
|
+
|
|
95
|
+
# Step 2: Spec generation
|
|
96
|
+
p "${BOLD}[2/10]${RESET} Generating spec with ${CYAN}glm/glm-4.5-air${RESET}..."
|
|
97
|
+
pause 2.0
|
|
98
|
+
pp " ${GRAY}▸ writing spec"; pause 0.3; pp "."; pause 0.3; pp "."; pause 0.3; pp ".${RESET}"; nl
|
|
99
|
+
pause 0.8
|
|
100
|
+
p " ${BGREEN}✔${RESET} Spec generated ${GRAY}(feature-task-management-v1.md)${RESET}"
|
|
101
|
+
pause 0.3
|
|
102
|
+
p " ${BGREEN}✔${RESET} ${BOLD}8 tasks${RESET} decomposed ${GRAY}(data → service → api → view → route → test)${RESET}"
|
|
103
|
+
nl
|
|
104
|
+
pause 0.7
|
|
105
|
+
|
|
106
|
+
# Step 3: Refinement
|
|
107
|
+
p "${BOLD}[3/10]${RESET} Interactive spec refinement..."
|
|
108
|
+
pause 1.0
|
|
109
|
+
p " ${CYAN}AI Changes${RESET} ── ${BGREEN}+18${RESET} ${RED}-4${RESET} lines"
|
|
110
|
+
p " ${GRAY} + Added: bulk delete behavior, export CSV endpoint${RESET}"
|
|
111
|
+
p " ${GRAY} + Added: permission check (admin only for delete)${RESET}"
|
|
112
|
+
p " ${GRAY} - Removed: redundant status filter duplicate${RESET}"
|
|
113
|
+
pause 0.5
|
|
114
|
+
p " ${BGREEN}✔${RESET} Spec refined and approved"
|
|
115
|
+
nl
|
|
116
|
+
pause 0.7
|
|
117
|
+
|
|
118
|
+
# Step 3.4: Quality assessment
|
|
119
|
+
p "${BOLD}[3.4/10]${RESET} Spec quality assessment..."
|
|
120
|
+
pause 0.8
|
|
121
|
+
pp " Coverage ["; score_bar 9; p "] ${BOLD}9${RESET}/10"
|
|
122
|
+
pause 0.2
|
|
123
|
+
pp " Clarity ["; score_bar 8; p "] ${BOLD}8${RESET}/10"
|
|
124
|
+
pause 0.2
|
|
125
|
+
pp " Constitution ["; score_bar 9; p "] ${BOLD}9${RESET}/10"
|
|
126
|
+
pause 0.4
|
|
127
|
+
p " ${BGREEN}✔${RESET} Quality gate passed ${GRAY}(minSpecScore: 7)${RESET}"
|
|
128
|
+
nl
|
|
129
|
+
pause 0.7
|
|
130
|
+
|
|
131
|
+
# Approval Gate
|
|
132
|
+
p "${BOLD}[Gate]${RESET} ${BYELLOW}Approval Gate${RESET} — review spec + DSL summary"
|
|
133
|
+
pause 0.5
|
|
134
|
+
p " ${GRAY}Spec:${RESET} Add task management table view with filters, bulk actions, CSV export"
|
|
135
|
+
p " ${GRAY}DSL preview:${RESET} Models: 3 · Endpoints: 7 · Behaviors: 3"
|
|
136
|
+
pause 0.5
|
|
137
|
+
p " ${BGREEN}✔${RESET} Proceeding..."
|
|
138
|
+
nl
|
|
139
|
+
pause 0.6
|
|
140
|
+
|
|
141
|
+
# DSL extraction
|
|
142
|
+
p "${BOLD}[DSL]${RESET} Extracting structured contract..."
|
|
143
|
+
pause 1.2
|
|
144
|
+
p " ${BGREEN}✔${RESET} DSL valid — Models: ${BOLD}3${RESET} Endpoints: ${BOLD}7${RESET} Behaviors: ${BOLD}3${RESET}"
|
|
145
|
+
p " ${GRAY} → feature-task-management-v1.dsl.json${RESET}"
|
|
146
|
+
nl
|
|
147
|
+
pause 0.7
|
|
148
|
+
|
|
149
|
+
# Git isolation
|
|
150
|
+
p "${BOLD}[Git]${RESET} Creating worktree branch..."
|
|
151
|
+
pause 0.6
|
|
152
|
+
p " ${BGREEN}✔${RESET} Branch: ${CYAN}feat/task-management${RESET} ${GRAY}(isolated from main)${RESET}"
|
|
153
|
+
nl
|
|
154
|
+
pause 0.6
|
|
155
|
+
|
|
156
|
+
# Step 6: Codegen
|
|
157
|
+
p "${BOLD}[6/10]${RESET} Code generation ${GRAY}(task-by-task, 8 files)${RESET}"
|
|
158
|
+
nl
|
|
159
|
+
|
|
160
|
+
local tasks=(
|
|
161
|
+
"data · Task.type.ts ${GRAY}types & interfaces${RESET}"
|
|
162
|
+
"service · src/api/task.ts ${GRAY}HTTP client layer${RESET}"
|
|
163
|
+
"api · src/stores/taskStore.ts ${GRAY}Pinia store + actions${RESET}"
|
|
164
|
+
"view · src/views/TaskList.vue ${GRAY}table + filters + bulk select${RESET}"
|
|
165
|
+
"view · src/views/TaskDetail.vue ${GRAY}detail panel component${RESET}"
|
|
166
|
+
"route · src/router/task.route.ts ${GRAY}route module${RESET}"
|
|
167
|
+
"test · tests/taskStore.test.ts ${GRAY}unit tests${RESET}"
|
|
168
|
+
"test · tests/TaskList.test.ts ${GRAY}component tests${RESET}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for task in "${tasks[@]}"; do
|
|
172
|
+
pause 0.55
|
|
173
|
+
p " ${BGREEN}✔${RESET} ${task}"
|
|
174
|
+
done
|
|
175
|
+
|
|
176
|
+
nl
|
|
177
|
+
pause 0.4
|
|
178
|
+
|
|
179
|
+
# Progress bar
|
|
180
|
+
pp " "; bar_full; p " ${BOLD}100%${RESET} → ${BGREEN}8/8 files written${RESET}"
|
|
181
|
+
nl
|
|
182
|
+
pause 0.8
|
|
183
|
+
|
|
184
|
+
# Step 7: Test skeleton
|
|
185
|
+
p "${BOLD}[7/10]${RESET} Test skeleton generated"
|
|
186
|
+
pause 0.5
|
|
187
|
+
p " ${BGREEN}✔${RESET} 2 test files · ${BOLD}14 test cases${RESET} scaffolded"
|
|
188
|
+
nl
|
|
189
|
+
pause 0.6
|
|
190
|
+
|
|
191
|
+
# Step 8: Error feedback
|
|
192
|
+
p "${BOLD}[8/10]${RESET} Error feedback loop..."
|
|
193
|
+
pause 0.8
|
|
194
|
+
p " ${YELLOW}⚠${RESET} Cycle 1 — ${BOLD}3 errors${RESET} detected"
|
|
195
|
+
p " ${GRAY} src/stores/taskStore.ts:12 — import 'fetchTasks' not found in api/task.ts${RESET}"
|
|
196
|
+
p " ${GRAY} src/stores/taskStore.ts:31 — Property 'total' missing on TaskResponse${RESET}"
|
|
197
|
+
p " ${GRAY} src/views/TaskList.vue:87 — Type mismatch: string vs TaskStatus enum${RESET}"
|
|
198
|
+
pause 1.0
|
|
199
|
+
pp " ${GRAY}▸ AI auto-fixing"; pause 0.3; pp "."; pause 0.3; pp "."; pause 0.3; pp ".${RESET}"; nl
|
|
200
|
+
pause 1.0
|
|
201
|
+
p " ${BGREEN}✔${RESET} All errors resolved in ${BOLD}1 cycle${RESET}"
|
|
202
|
+
nl
|
|
203
|
+
pause 0.7
|
|
204
|
+
|
|
205
|
+
# Step 9: 3-pass review
|
|
206
|
+
p "${BOLD}[9/10]${RESET} 3-pass code review"
|
|
207
|
+
pause 0.6
|
|
208
|
+
p " ${BOLD}Pass 0${RESET} Spec compliance → ${BGREEN}✔ aligned${RESET}"
|
|
209
|
+
pause 0.4
|
|
210
|
+
p " ${BOLD}Pass 1${RESET} Architecture audit → ${BGREEN}✔ layer separation correct${RESET}"
|
|
211
|
+
pause 0.4
|
|
212
|
+
p " ${BOLD}Pass 2${RESET} Implementation → ${YELLOW}⚠ 1 issue: missing pagination guard${RESET}"
|
|
213
|
+
pause 0.4
|
|
214
|
+
p " ${BOLD}Pass 3${RESET} Impact & complexity → ${BGREEN}Low impact · Low complexity${RESET}"
|
|
215
|
+
pause 0.5
|
|
216
|
+
pp " Score ["; score_bar 8; p "] ${BOLD}8.2${RESET}/10"
|
|
217
|
+
nl
|
|
218
|
+
pause 0.7
|
|
219
|
+
|
|
220
|
+
# Step 10: Harness Self-Eval
|
|
221
|
+
p "${BOLD}[10/10]${RESET} Harness Self-Eval"
|
|
222
|
+
pause 0.6
|
|
223
|
+
p " Compliance ${GRAY}(30%)${RESET} → ${BGREEN}28/30${RESET}"
|
|
224
|
+
pause 0.2
|
|
225
|
+
p " DSL Coverage ${GRAY}(25%)${RESET} → ${BGREEN}23/25${RESET}"
|
|
226
|
+
pause 0.2
|
|
227
|
+
p " Compile ${GRAY}(20%)${RESET} → ${BGREEN}20/20${RESET}"
|
|
228
|
+
pause 0.2
|
|
229
|
+
p " Review ${GRAY}(25%)${RESET} → ${BGREEN}21/25${RESET}"
|
|
230
|
+
pause 0.4
|
|
231
|
+
pp " Total ["; score_bar 9; p "] ${BOLD}92 / 100${RESET}"
|
|
232
|
+
nl
|
|
233
|
+
pause 0.5
|
|
234
|
+
p " ${BGREEN}✔${RESET} ${BOLD}2 lesson(s)${RESET} → constitution §9"
|
|
235
|
+
p " ${BGREEN}✔${RESET} RunId: ${CYAN}20260408-143022-a7f2${RESET} ${GRAY}· 8 files written · 94.3s${RESET}"
|
|
236
|
+
nl
|
|
237
|
+
pause 0.5
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# ── Scene 3: multi-repo workspace ─────────────────────────────────────────────
|
|
241
|
+
scene_multirepo() {
|
|
242
|
+
nl
|
|
243
|
+
p "${BCYAN}┌─────────────────────────────────────────────────┐${RESET}"
|
|
244
|
+
p "${BCYAN}│ ai-spec · Multi-Repo Workspace Mode │${RESET}"
|
|
245
|
+
p "${BCYAN}└─────────────────────────────────────────────────┘${RESET}"
|
|
246
|
+
nl
|
|
247
|
+
pause 0.5
|
|
248
|
+
|
|
249
|
+
# Repo selection
|
|
250
|
+
p "${BOLD}[Repo]${RESET} Select repo(s) for this feature:"
|
|
251
|
+
pause 0.4
|
|
252
|
+
p " ${BGREEN}●${RESET} rushbuy-node-service ${GRAY}(node-express / backend)${RESET}"
|
|
253
|
+
p " ${BGREEN}●${RESET} rushbuy-web-admin ${GRAY}(vue / frontend)${RESET}"
|
|
254
|
+
pause 0.5
|
|
255
|
+
p " ${BGREEN}✔${RESET} ${BOLD}2 repos selected${RESET} ${GRAY}→ workspace mode activated${RESET}"
|
|
256
|
+
nl
|
|
257
|
+
pause 0.6
|
|
258
|
+
|
|
259
|
+
# AI responsibility split
|
|
260
|
+
p "${BOLD}[W1]${RESET} AI splitting responsibilities..."
|
|
261
|
+
pause 1.0
|
|
262
|
+
p " ${GRAY}Backend →${RESET} user profile CRUD endpoints, avatar upload, preferences schema"
|
|
263
|
+
p " ${GRAY}Frontend →${RESET} profile settings page, avatar cropper, real-time form validation"
|
|
264
|
+
p " ${GRAY}UX decision:${RESET} modal-based edit (not full-page redirect)"
|
|
265
|
+
nl
|
|
266
|
+
pause 0.7
|
|
267
|
+
|
|
268
|
+
# Backend pipeline summary
|
|
269
|
+
p "${BOLD}[W2]${RESET} ${CYAN}Backend pipeline${RESET} ${GRAY}(rushbuy-node-service)${RESET}"
|
|
270
|
+
pause 0.4
|
|
271
|
+
p " ${BGREEN}✔${RESET} Spec generated ${GRAY}·${RESET} DSL extracted"
|
|
272
|
+
p " ${BGREEN}✔${RESET} Models: ${BOLD}2${RESET} · Endpoints: ${BOLD}5${RESET} · Behaviors: ${BOLD}2${RESET}"
|
|
273
|
+
pause 0.3
|
|
274
|
+
p " ${BGREEN}✔${RESET} Code generated ${GRAY}(6 files · 0 errors)${RESET}"
|
|
275
|
+
pp " Score ["; score_bar 9; p "] ${BOLD}90 / 100${RESET}"
|
|
276
|
+
nl
|
|
277
|
+
pause 0.7
|
|
278
|
+
|
|
279
|
+
# DSL contract handoff
|
|
280
|
+
p "${BOLD}[W3]${RESET} ${BYELLOW}DSL contract handoff${RESET} ${GRAY}→ injecting into frontend pipeline${RESET}"
|
|
281
|
+
pause 0.6
|
|
282
|
+
p " ${GRAY}Backend DSL endpoints passed to frontend:${RESET}"
|
|
283
|
+
p " ${BBLUE} GET${RESET} /api/users/:id/profile"
|
|
284
|
+
p " ${BGREEN} PUT${RESET} /api/users/:id/profile"
|
|
285
|
+
p " ${BMAGENTA} POST${RESET} /api/users/:id/avatar"
|
|
286
|
+
p " ${RED} DEL${RESET} /api/users/:id/avatar"
|
|
287
|
+
p " ${BBLUE} GET${RESET} /api/users/:id/preferences"
|
|
288
|
+
nl
|
|
289
|
+
pause 0.7
|
|
290
|
+
|
|
291
|
+
# Frontend pipeline summary
|
|
292
|
+
p "${BOLD}[W4]${RESET} ${CYAN}Frontend pipeline${RESET} ${GRAY}(rushbuy-web-admin)${RESET}"
|
|
293
|
+
pause 0.4
|
|
294
|
+
p " ${BGREEN}✔${RESET} Spec generated ${GRAY}(injected with backend DSL contract)${RESET}"
|
|
295
|
+
p " ${BGREEN}✔${RESET} Code generated ${GRAY}(8 files · 1 error → auto-fixed)${RESET}"
|
|
296
|
+
pp " Score ["; score_bar 8; p "] ${BOLD}87 / 100${RESET}"
|
|
297
|
+
nl
|
|
298
|
+
pause 0.7
|
|
299
|
+
|
|
300
|
+
# Cross-stack verifier
|
|
301
|
+
p "${BOLD}[W5]${RESET} ${BMAGENTA}Cross-stack contract verification${RESET}"
|
|
302
|
+
pause 0.8
|
|
303
|
+
p " Scanning frontend API calls vs backend DSL..."
|
|
304
|
+
pause 0.8
|
|
305
|
+
p " ${BGREEN}✔${RESET} Matched : ${BOLD}5 / 5${RESET} endpoints"
|
|
306
|
+
p " ${BGREEN}✔${RESET} Phantoms : ${BOLD}0${RESET} ${GRAY}(no hallucinated routes)${RESET}"
|
|
307
|
+
p " ${BGREEN}✔${RESET} Mismatches: ${BOLD}0${RESET} ${GRAY}(HTTP methods all correct)${RESET}"
|
|
308
|
+
pause 0.4
|
|
309
|
+
p " ${BGREEN}✔${RESET} Cross-stack contract ${BOLD}CLEAN${RESET}"
|
|
310
|
+
nl
|
|
311
|
+
pause 0.5
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# ── Scene 4: DSL artifacts ─────────────────────────────────────────────────────
|
|
315
|
+
scene_artifacts() {
|
|
316
|
+
nl
|
|
317
|
+
p "${BCYAN}┌─────────────────────────────────────────────────┐${RESET}"
|
|
318
|
+
p "${BCYAN}│ ai-spec · DSL-Derived Artifacts │${RESET}"
|
|
319
|
+
p "${BCYAN}└─────────────────────────────────────────────────┘${RESET}"
|
|
320
|
+
nl
|
|
321
|
+
pause 0.5
|
|
322
|
+
|
|
323
|
+
# OpenAPI export
|
|
324
|
+
p "${BOLD}$ ai-spec export${RESET}"
|
|
325
|
+
pause 0.8
|
|
326
|
+
p " ${BGREEN}✔${RESET} Loaded DSL ${GRAY}— Models: 3 Endpoints: 7 Behaviors: 3${RESET}"
|
|
327
|
+
pause 0.4
|
|
328
|
+
p " ${BGREEN}✔${RESET} Generated: ${CYAN}openapi.yaml${RESET} ${GRAY}(OpenAPI 3.1.0)${RESET}"
|
|
329
|
+
pause 0.3
|
|
330
|
+
p " ${GRAY} openapi: 3.1.0${RESET}"
|
|
331
|
+
p " ${GRAY} info: { title: rushbuy-api, version: 1.0.0 }${RESET}"
|
|
332
|
+
p " ${GRAY} paths: { /api/tasks, /api/tasks/:id, /api/tasks/bulk, ... }${RESET}"
|
|
333
|
+
p " ${GRAY} → plug into Postman · Swagger UI · SDK generators${RESET}"
|
|
334
|
+
nl
|
|
335
|
+
pause 0.8
|
|
336
|
+
|
|
337
|
+
# Types generation
|
|
338
|
+
p "${BOLD}$ ai-spec types${RESET}"
|
|
339
|
+
pause 0.8
|
|
340
|
+
p " ${BGREEN}✔${RESET} Generated: ${CYAN}src/types/api-contracts.ts${RESET}"
|
|
341
|
+
pause 0.2
|
|
342
|
+
p " ${GRAY} export interface Task \{ id, title, status, assignee, dueDate \}${RESET}"
|
|
343
|
+
p " ${GRAY} export type TaskStatus = 'todo' | 'in_progress' | 'done' | 'cancelled'${RESET}"
|
|
344
|
+
p " ${GRAY} export const API_ENDPOINTS = \{ TASK_LIST: '/api/tasks', ... \}${RESET}"
|
|
345
|
+
nl
|
|
346
|
+
pause 0.8
|
|
347
|
+
|
|
348
|
+
# Mock server
|
|
349
|
+
p "${BOLD}$ ai-spec mock --serve --port 3001${RESET}"
|
|
350
|
+
pause 0.8
|
|
351
|
+
p " ${BGREEN}✔${RESET} Generated: ${CYAN}mock/server.js${RESET} ${GRAY}(Express mock server)${RESET}"
|
|
352
|
+
p " ${BGREEN}✔${RESET} Generated: ${CYAN}mock/handlers.ts${RESET} ${GRAY}(MSW handlers)${RESET}"
|
|
353
|
+
p " ${BGREEN}✔${RESET} Patched: ${CYAN}vite.config.ts${RESET} ${GRAY}(proxy /api → :3001)${RESET}"
|
|
354
|
+
pause 0.5
|
|
355
|
+
p " ${BGREEN}▶${RESET} Mock server running on ${BCYAN}http://localhost:3001${RESET}"
|
|
356
|
+
p " ${GRAY} GET /api/tasks → 200 [seed: 10 tasks]${RESET}"
|
|
357
|
+
p " ${GRAY} POST /api/tasks → 201 \{ id, title, status \}${RESET}"
|
|
358
|
+
p " ${GRAY} PUT /api/tasks/:id → 200 updated task${RESET}"
|
|
359
|
+
p " ${GRAY} DEL /api/tasks/:id → 204 no content${RESET}"
|
|
360
|
+
nl
|
|
361
|
+
pause 0.5
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# ── Scene 5: observability ─────────────────────────────────────────────────────
|
|
365
|
+
scene_observability() {
|
|
366
|
+
nl
|
|
367
|
+
p "${BCYAN}┌─────────────────────────────────────────────────┐${RESET}"
|
|
368
|
+
p "${BCYAN}│ ai-spec · Observability Layer │${RESET}"
|
|
369
|
+
p "${BCYAN}└─────────────────────────────────────────────────┘${RESET}"
|
|
370
|
+
nl
|
|
371
|
+
pause 0.5
|
|
372
|
+
|
|
373
|
+
# Logs
|
|
374
|
+
p "${BOLD}$ ai-spec logs${RESET}"
|
|
375
|
+
pause 0.8
|
|
376
|
+
p ""
|
|
377
|
+
p " ${BOLD}RunId ${GRAY}Date ${RESET}${BOLD}Files Score Duration${RESET}"
|
|
378
|
+
p " ${CYAN}20260408-143022-a7f2${RESET} ${GRAY}2026-04-08${RESET} 8 ${BGREEN}92${RESET} 94s"
|
|
379
|
+
p " ${CYAN}20260407-101455-b3c1${RESET} ${GRAY}2026-04-07${RESET} 6 ${BGREEN}88${RESET} 81s"
|
|
380
|
+
p " ${CYAN}20260406-174230-d9e5${RESET} ${GRAY}2026-04-06${RESET} 11 ${YELLOW}74${RESET} 128s"
|
|
381
|
+
p " ${CYAN}20260405-093010-f1a8${RESET} ${GRAY}2026-04-05${RESET} 5 ${BGREEN}85${RESET} 67s"
|
|
382
|
+
nl
|
|
383
|
+
pause 0.8
|
|
384
|
+
|
|
385
|
+
# Trend
|
|
386
|
+
p "${BOLD}$ ai-spec trend${RESET}"
|
|
387
|
+
pause 0.8
|
|
388
|
+
p ""
|
|
389
|
+
p " Harness Score Trend ${GRAY}(last 5 runs)${RESET}"
|
|
390
|
+
p ""
|
|
391
|
+
p " 100 ┤"
|
|
392
|
+
p " 90 ┤ ${BGREEN}●${RESET}──────────────────${BGREEN}●${RESET}"
|
|
393
|
+
p " 80 ┤ ${BGREEN}●${RESET}──${BGREEN}●${RESET}"
|
|
394
|
+
p " 70 ┤──${YELLOW}●${RESET}"
|
|
395
|
+
p " 60 ┤"
|
|
396
|
+
p " └────────────────────────────── runs →"
|
|
397
|
+
p " Apr 5 Apr 6 Apr 7 Apr 8"
|
|
398
|
+
nl
|
|
399
|
+
p " ${BGREEN}↑ +18 points${RESET} over last 4 runs ${GRAY}(constitution learning in effect)${RESET}"
|
|
400
|
+
nl
|
|
401
|
+
pause 0.5
|
|
402
|
+
|
|
403
|
+
# Restore hint
|
|
404
|
+
p " ${GRAY}Tip: ai-spec restore 20260406-174230-d9e5 → rollback that run instantly${RESET}"
|
|
405
|
+
nl
|
|
406
|
+
pause 0.5
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# ── Main ───────────────────────────────────────────────────────────────────────
|
|
410
|
+
SCENE="${1:-all}"
|
|
411
|
+
|
|
412
|
+
case "$SCENE" in
|
|
413
|
+
help) scene_help ;;
|
|
414
|
+
create) scene_create ;;
|
|
415
|
+
multirepo) scene_multirepo ;;
|
|
416
|
+
artifacts) scene_artifacts ;;
|
|
417
|
+
observability) scene_observability ;;
|
|
418
|
+
all)
|
|
419
|
+
scene_help
|
|
420
|
+
sleep 1
|
|
421
|
+
scene_create
|
|
422
|
+
sleep 1
|
|
423
|
+
scene_multirepo
|
|
424
|
+
sleep 1
|
|
425
|
+
scene_artifacts
|
|
426
|
+
sleep 1
|
|
427
|
+
scene_observability
|
|
428
|
+
;;
|
|
429
|
+
*)
|
|
430
|
+
echo "Usage: $0 [help|create|multirepo|artifacts|observability|all]"
|
|
431
|
+
exit 1
|
|
432
|
+
;;
|
|
433
|
+
esac
|