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 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.55.0-blue" alt="version" />
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 (phantomCount === 0 && mismatchCount === 0 && unusedCount === 0 && matchedCount === totalEp && totalEp > 0) {
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