ai-spec-dev 0.33.0 → 0.36.1

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.
Files changed (64) hide show
  1. package/.claude/commands/add-lesson.md +34 -0
  2. package/.claude/commands/check-layers.md +65 -0
  3. package/.claude/commands/installed-deps.md +35 -0
  4. package/.claude/commands/recall-lessons.md +40 -0
  5. package/.claude/commands/scan-singletons.md +45 -0
  6. package/.claude/commands/verify-imports.md +48 -0
  7. package/.claude/settings.local.json +11 -1
  8. package/README.md +531 -213
  9. package/RELEASE_LOG.md +424 -0
  10. package/cli/commands/config.ts +18 -0
  11. package/cli/commands/create.ts +1248 -0
  12. package/cli/commands/dashboard.ts +62 -0
  13. package/cli/commands/init.ts +45 -8
  14. package/cli/commands/mock.ts +175 -0
  15. package/cli/commands/scan.ts +99 -0
  16. package/cli/commands/types.ts +69 -0
  17. package/cli/commands/vcr.ts +70 -0
  18. package/cli/index.ts +34 -2517
  19. package/cli/utils.ts +4 -0
  20. package/core/code-generator.ts +6 -4
  21. package/core/combined-generator.ts +13 -3
  22. package/core/dashboard-generator.ts +340 -0
  23. package/core/design-dialogue.ts +124 -0
  24. package/core/dsl-extractor.ts +9 -1
  25. package/core/dsl-feedback.ts +41 -5
  26. package/core/dsl-validator.ts +32 -0
  27. package/core/error-feedback.ts +46 -2
  28. package/core/key-store.ts +5 -4
  29. package/core/project-index.ts +301 -0
  30. package/core/provider-utils.ts +39 -4
  31. package/core/reviewer.ts +84 -6
  32. package/core/run-logger.ts +109 -3
  33. package/core/run-trend.ts +24 -4
  34. package/core/self-evaluator.ts +39 -11
  35. package/core/spec-generator.ts +14 -8
  36. package/core/task-generator.ts +17 -0
  37. package/core/types-generator.ts +219 -0
  38. package/core/vcr.ts +210 -0
  39. package/dist/cli/index.js +7407 -5643
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/cli/index.mjs +7401 -5637
  42. package/dist/cli/index.mjs.map +1 -1
  43. package/dist/index.d.mts +34 -5
  44. package/dist/index.d.ts +34 -5
  45. package/dist/index.js +497 -232
  46. package/dist/index.js.map +1 -1
  47. package/dist/index.mjs +495 -233
  48. package/dist/index.mjs.map +1 -1
  49. package/docs-assets/purpose/architecture-overview.svg +64 -0
  50. package/docs-assets/purpose/create-pipeline.svg +113 -0
  51. package/docs-assets/purpose/task-layering.svg +74 -0
  52. package/package.json +1 -1
  53. package/prompts/codegen.prompt.ts +97 -9
  54. package/prompts/design.prompt.ts +59 -0
  55. package/prompts/spec.prompt.ts +8 -1
  56. package/prompts/tasks.prompt.ts +27 -2
  57. package/purpose.md +600 -174
  58. package/tests/code-generator.test.ts +253 -0
  59. package/tests/context-loader.test.ts +207 -0
  60. package/tests/dsl-validator.test.ts +105 -0
  61. package/tests/openapi-exporter.test.ts +310 -0
  62. package/tests/reviewer.test.ts +214 -0
  63. package/tests/spec-generator.test.ts +228 -0
  64. package/tests/spec-versioning.test.ts +205 -0
package/dist/index.js CHANGED
@@ -50,6 +50,9 @@ __export(index_exports, {
50
50
  TaskGenerator: () => TaskGenerator,
51
51
  buildTaskPrompt: () => buildTaskPrompt,
52
52
  createProvider: () => createProvider,
53
+ extractBehavioralContract: () => extractBehavioralContract,
54
+ extractComplianceScore: () => extractComplianceScore,
55
+ extractMissingCount: () => extractMissingCount,
53
56
  generateSpecWithTasks: () => generateSpecWithTasks,
54
57
  isFrontendDeps: () => isFrontendDeps,
55
58
  loadConstitution: () => loadConstitution,
@@ -170,7 +173,14 @@ model ExampleModel {
170
173
 
171
174
  ---
172
175
 
173
- \u6839\u636E\u7528\u6237\u7684\u60F3\u6CD5\u548C\u9879\u76EE\u4E0A\u4E0B\u6587\u751F\u6210\u4E0A\u8FF0\u5B8C\u6574 Spec\u3002\u786E\u4FDD API \u8BBE\u8BA1\u4E0E\u73B0\u6709\u9879\u76EE\u7684\u8DEF\u7531\u98CE\u683C\u3001\u9519\u8BEF\u7801\u89C4\u8303\u4FDD\u6301\u4E00\u81F4\uFF0C\u6570\u636E\u6A21\u578B\u4E0E\u73B0\u6709 Prisma Schema \u534F\u8C03\u3002`;
176
+ \u6839\u636E\u7528\u6237\u7684\u60F3\u6CD5\u548C\u9879\u76EE\u4E0A\u4E0B\u6587\u751F\u6210\u4E0A\u8FF0\u5B8C\u6574 Spec\u3002\u786E\u4FDD API \u8BBE\u8BA1\u4E0E\u73B0\u6709\u9879\u76EE\u7684\u8DEF\u7531\u98CE\u683C\u3001\u9519\u8BEF\u7801\u89C4\u8303\u4FDD\u6301\u4E00\u81F4\uFF0C\u6570\u636E\u6A21\u578B\u4E0E\u73B0\u6709 Prisma Schema \u534F\u8C03\u3002
177
+
178
+ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uFF09\uFF1A
179
+ \u5982\u679C\u9879\u76EE\u5BAA\u6CD5\u4E2D\u5305\u542B"\xA79 \u79EF\u7D2F\u6559\u8BAD (Accumulated Lessons)"\u7AE0\u8282\uFF0C\u4F60\u5FC5\u987B\uFF1A
180
+ 1. \u5728\u751F\u6210 \xA75 API \u8BBE\u8BA1\u548C \xA76 \u6570\u636E\u6A21\u578B\u4E4B\u524D\uFF0C\u9010\u6761\u5BA1\u9605\u6240\u6709\u6559\u8BAD\u6761\u76EE
181
+ 2. \u786E\u4FDD\u672C\u6B21 Spec \u7684\u8BBE\u8BA1\u4E0D\u91CD\u8E48\u5DF2\u77E5\u95EE\u9898\uFF08\u4F8B\u5982\uFF1A\u67D0\u6559\u8BAD\u8BF4"\u907F\u514D N+1 \u67E5\u8BE2"\uFF0C\u5219\u5728 \xA78 \u5B9E\u65BD\u8981\u70B9\u4E2D\u660E\u786E\u8BF4\u660E\u6279\u91CF\u52A0\u8F7D\u7B56\u7565\uFF09
182
+ 3. \u5BF9\u4E8E\u6BCF\u6761\u76F4\u63A5\u76F8\u5173\u7684\u6559\u8BAD\uFF0C\u5728 \xA78 \u5B9E\u65BD\u8981\u70B9\u672B\u5C3E\u8FFD\u52A0\u4E00\u884C\uFF1A\u300C\u26A0 \u57FA\u4E8E\u5386\u53F2\u6559\u8BAD\uFF1A[\u7B80\u8FF0\u672C\u6B21 spec \u5982\u4F55\u89C4\u907F\u8BE5\u95EE\u9898]\u300D
183
+ 4. \u5982\u65E0\u76F8\u5173\u6559\u8BAD\uFF0C\xA78 \u4E0D\u5FC5\u8FFD\u52A0\u4EFB\u4F55\u5185\u5BB9`;
174
184
 
175
185
  // core/provider-utils.ts
176
186
  var import_chalk = __toESM(require("chalk"));
@@ -187,14 +197,49 @@ function classifyError(err, label) {
187
197
  const e = err;
188
198
  const status = e.status ?? e.response?.status;
189
199
  if (status === 401 || status === 403)
190
- return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
200
+ return new ProviderError(
201
+ `Auth error (${label}): API key is invalid or expired.
202
+ \u2192 Check that the correct API key is set in your environment or ~/.ai-spec-keys.json
203
+ \u2192 Run "ai-spec model" to reconfigure your provider and key`,
204
+ "auth",
205
+ err
206
+ );
191
207
  if (status === 429)
192
- return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
208
+ return new ProviderError(
209
+ `Rate limit hit (${label}): too many requests.
210
+ \u2192 Wait a few minutes and retry, or switch to a different provider/model
211
+ \u2192 Check your provider's billing dashboard for quota status`,
212
+ "rate_limit",
213
+ err
214
+ );
193
215
  if (e._timeout || e.message?.toLowerCase().includes("timed out"))
194
216
  return new ProviderError(`Request timed out (${label})`, "timeout", err);
195
217
  if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
196
- return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
197
- return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
218
+ return new ProviderError(
219
+ `Network error (${label}): ${e.message}
220
+ \u2192 Check your internet connection and proxy settings (HTTPS_PROXY)
221
+ \u2192 If behind a firewall, ensure the provider's API endpoint is reachable`,
222
+ "network",
223
+ err
224
+ );
225
+ const msg = e.message ?? "";
226
+ if (status === 404 || msg.includes("model") && (msg.includes("not found") || msg.includes("does not exist")))
227
+ return new ProviderError(
228
+ `Model not found (${label}): ${msg}
229
+ \u2192 Run "ai-spec model" to see available models for your provider
230
+ \u2192 The model name may have changed \u2014 check your provider's documentation`,
231
+ "provider",
232
+ err
233
+ );
234
+ if (msg.includes("insufficient") || msg.includes("quota") || msg.includes("balance"))
235
+ return new ProviderError(
236
+ `Quota/balance error (${label}): ${msg}
237
+ \u2192 Check your provider's billing dashboard
238
+ \u2192 Consider switching to a different provider with "ai-spec model"`,
239
+ "provider",
240
+ err
241
+ );
242
+ return new ProviderError(`Provider error (${label}): ${msg}`, "provider", err);
198
243
  }
199
244
  function isRetryable(err) {
200
245
  const e = err;
@@ -321,17 +366,23 @@ var PROVIDER_CATALOG = {
321
366
  },
322
367
  glm: {
323
368
  displayName: "\u667A\u8C31 GLM (Zhipu AI)",
324
- description: "\u667A\u8C31 AI \u2014 GLM-5 / GLM-4 series + Z1 reasoning",
369
+ description: "\u667A\u8C31 AI \u2014 GLM-5.1 / GLM-5 / GLM-4 series",
325
370
  models: [
371
+ "glm-5.1",
372
+ // GLM-5.1 — latest flagship (2026)
326
373
  "glm-5",
327
- // GLM-5 flagship (如不可用请确认最新 model ID)
328
- "glm-5-flash",
374
+ // GLM-5 premium (Max/Pro plans)
375
+ "glm-5-turbo",
376
+ // GLM-5-Turbo — fast & cost-efficient
377
+ "glm-4.7",
378
+ // GLM-4.7
379
+ "glm-4.6",
380
+ // GLM-4.6
381
+ "glm-4.5-air",
382
+ // GLM-4.5-Air — lightweight
329
383
  "glm-z1",
330
- // GLM-Z1 reasoning model
331
- "glm-z1-flash",
332
- "glm-4-plus",
333
- "glm-4-flash",
334
- "glm-4-long"
384
+ // GLM-Z1 reasoning model
385
+ "glm-z1-flash"
335
386
  ],
336
387
  envKey: "ZHIPU_API_KEY",
337
388
  baseURL: "https://open.bigmodel.cn/api/paas/v4/"
@@ -527,8 +578,15 @@ var SpecGenerator = class {
527
578
  constructor(provider) {
528
579
  this.provider = provider;
529
580
  }
530
- async generateSpec(idea, context) {
581
+ async generateSpec(idea, context, architectureDecision) {
531
582
  const parts = [idea];
583
+ if (architectureDecision) {
584
+ parts.push(
585
+ `
586
+ === Architecture Decision (MUST follow this approach in the spec) ===
587
+ ${architectureDecision}`
588
+ );
589
+ }
532
590
  if (context) {
533
591
  if (context.constitution) {
534
592
  parts.push(
@@ -4283,34 +4341,110 @@ function getCodeGenSystemPrompt(repoType) {
4283
4341
  return codeGenSystemPrompt;
4284
4342
  }
4285
4343
  }
4286
- var reviewArchitectureSystemPrompt = `You are a Senior Software Architect reviewing the HIGH-LEVEL design of a code change.
4344
+ var specComplianceSystemPrompt = `You are a QA Engineer performing a SPEC COMPLIANCE CHECK.
4345
+
4346
+ Your sole job is to verify that the implementation covers every requirement stated in the feature spec.
4347
+ This is a completeness audit \u2014 NOT a code quality review. Do not comment on architecture, style, or implementation details.
4348
+
4349
+ ## How to audit:
4350
+
4351
+ 1. Parse the spec and extract EVERY stated requirement into these categories:
4352
+ - **Endpoints**: each HTTP method + path listed or implied
4353
+ - **Data Models**: each entity, field, constraint mentioned
4354
+ - **Business Rules**: validations, conditions, calculations stated
4355
+ - **Auth Requirements**: which endpoints need auth, which roles are allowed
4356
+ - **Error Cases**: explicit error codes or failure scenarios mentioned
4357
+ - **Side Effects**: emails sent, events fired, caches invalidated, etc.
4358
+
4359
+ 2. For each extracted requirement, check the provided code:
4360
+ - \u2705 **Covered** \u2014 clearly implemented
4361
+ - \u26A0\uFE0F **Partial** \u2014 exists but incomplete (e.g. endpoint exists but missing a field or error case)
4362
+ - \u274C **Missing** \u2014 requirement stated in spec but not found in code
4363
+
4364
+ 3. Output a compliance checklist. Be exhaustive \u2014 list every single requirement.
4365
+
4366
+ ## Output format:
4367
+
4368
+ ## \u{1F4CB} Spec Compliance Report
4369
+
4370
+ ### Endpoints
4371
+ \u2705 / \u26A0\uFE0F / \u274C METHOD /path \u2014 one-line status
4372
+
4373
+ ### Data Models
4374
+ \u2705 / \u26A0\uFE0F / \u274C ModelName \u2014 one-line status
4375
+
4376
+ ### Business Rules
4377
+ \u2705 / \u26A0\uFE0F / \u274C Rule description \u2014 one-line status
4378
+
4379
+ ### Auth & Permissions
4380
+ \u2705 / \u26A0\uFE0F / \u274C Requirement \u2014 one-line status
4381
+
4382
+ ### Error Cases
4383
+ \u2705 / \u26A0\uFE0F / \u274C Error scenario \u2014 one-line status
4384
+
4385
+ ### Side Effects
4386
+ \u2705 / \u26A0\uFE0F / \u274C Side effect \u2014 one-line status (omit section if none in spec)
4387
+
4388
+ ---
4389
+
4390
+ ## \u{1F4CA} Compliance Summary
4391
+ Covered: N | Partial: N | Missing: N | Total: N
4392
+
4393
+ ## \u{1F522} Compliance Score
4394
+ ComplianceScore: X/10
4395
+
4396
+ (10 = all requirements implemented, 0 = nothing implemented.
4397
+ Deduct 1 point per missing requirement, 0.5 per partial.
4398
+ Round to nearest integer.)
4399
+
4400
+ ## \u{1F6A8} Blockers (Missing requirements that MUST be implemented before ship)
4401
+ List only \u274C Missing items here, ordered by severity. If none, write "None".
4402
+
4403
+ ---
4404
+
4405
+ IMPORTANT: Be exhaustive. A requirement not listed here is assumed to be covered.
4406
+ If the spec is vague, note the ambiguity as \u26A0\uFE0F Partial rather than assuming coverage.`;
4407
+ var reviewArchitectureSystemPrompt = `You are a Senior Software Architect reviewing the HIGH-LEVEL DESIGN of a code change.
4408
+
4409
+ A spec compliance check (Pass 0) has already verified feature completeness. Do NOT re-audit whether requirements are missing \u2014 focus purely on HOW the present implementation is architected.
4287
4410
 
4288
4411
  Focus ONLY on:
4289
- 1. **Spec compliance** \u2014 Does the implementation match the spec? Are there missing or extra endpoints/components?
4290
- 2. **Layer separation** \u2014 Does each layer have the right responsibilities? (e.g., no business logic in controllers, no HTTP in stores)
4291
- 3. **API contract** \u2014 Are request/response shapes correct? Are all error codes from the spec implemented?
4292
- 4. **Data model integrity** \u2014 Are constraints, unique fields, and relationships correct?
4293
- 5. **Security posture** \u2014 Are auth checks applied to the right endpoints? Any obvious missing auth?
4412
+ 1. **Layer separation** \u2014 Does each layer have the right responsibilities? (e.g., no business logic in controllers, no HTTP in stores)
4413
+ 2. **API contract quality** \u2014 Are request/response shapes well-designed? Are error codes consistent with project conventions?
4414
+ 3. **Data model integrity** \u2014 Are constraints, unique fields, and relationships modelled correctly?
4415
+ 4. **Security posture** \u2014 Are auth checks applied correctly? Any privilege escalation risks?
4294
4416
 
4295
4417
  DO NOT comment on:
4418
+ - Whether specific endpoints or features are missing (covered by Pass 0)
4296
4419
  - Code style, naming conventions, formatting
4297
4420
  - Minor implementation details (variable names, inline comments)
4298
4421
  - Performance micro-optimizations
4299
4422
 
4300
4423
  Format:
4301
4424
 
4302
- ## \u{1F3D7} \u67B6\u6784\u5408\u89C4\u6027 (Spec Compliance)
4303
- Does the implementation match the spec? List any missing or wrong endpoints/components.
4304
-
4305
4425
  ## \u{1F500} \u5C42\u804C\u8D23\u5206\u79BB (Layer Separation)
4306
4426
  Any layer boundary violations?
4307
4427
 
4308
4428
  ## \u{1F512} \u5B89\u5168\u4E0E\u6743\u9650 (Security & Auth)
4309
4429
  Any missing auth checks, exposed data, or privilege issues?
4310
4430
 
4431
+ ## \u{1F4D0} \u5951\u7EA6\u4E0E\u6A21\u578B\u8BBE\u8BA1 (Contract & Model Design)
4432
+ Response shape issues, missing constraints, relationship problems.
4433
+
4311
4434
  ## \u{1F4CB} \u67B6\u6784\u8BC4\u5206 (Architecture Score)
4312
4435
  Score: X/10 \u2014 One short paragraph.
4313
4436
 
4437
+ ## \u{1F50D} \u7ED3\u6784\u6027\u53D1\u73B0 JSON (Structural Findings \u2014 for pipeline processing)
4438
+ Output a JSON block with any design-level issues found above.
4439
+ Categories: "auth_design" | "api_contract" | "model_design" | "layer_violation" | "other_design"
4440
+ If no findings, output an empty array.
4441
+
4442
+ \`\`\`json
4443
+ {"structuralFindings": [{"category": "...", "description": "one sentence referencing the specific endpoint/model/file"}]}
4444
+ \`\`\`
4445
+
4446
+ IMPORTANT: Always include this JSON block, even when structuralFindings is []. This block is parsed by the pipeline.
4447
+
4314
4448
  Be specific. Reference file names or endpoint paths.`;
4315
4449
  var reviewImplementationSystemPrompt = `You are a Senior Engineer reviewing the IMPLEMENTATION DETAILS of a code change.
4316
4450
 
@@ -4403,7 +4537,8 @@ Each task object must have these exact fields:
4403
4537
  "description": "...", // 1-2 sentences, specific and actionable
4404
4538
  "layer": "data|service|api|view|route|test|infra", // implementation layer
4405
4539
  "filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
4406
- "acceptanceCriteria": ["..."], // verifiable completion conditions
4540
+ "acceptanceCriteria": ["..."], // behavioral completion conditions (the "what")
4541
+ "verificationSteps": ["..."], // concrete runnable checks with expected output (the "how to verify") \u2014 see rules below
4407
4542
  "dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
4408
4543
  "priority": "high|medium|low"
4409
4544
  }
@@ -4445,8 +4580,32 @@ CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
4445
4580
  - If you are unsure of the exact path for a new file, leave it as "TBD:<description>" rather than guessing.
4446
4581
  - Cross-check: after writing all tasks, verify every path in filesToTouch exists in the inventory or is a logical new sibling. If it doesn't pass this check, fix it.
4447
4582
 
4583
+ CRITICAL \u2014 verificationSteps Rules:
4584
+ Each step must be a concrete, self-contained check with an observable expected outcome.
4585
+
4586
+ Good examples (specific command + expected result):
4587
+ "POST /api/tasks with body {"title":"test"} \u2192 HTTP 201, response body contains {id, status:"pending"}"
4588
+ "GET /api/tasks/:id with unknown id \u2192 HTTP 404 with {code: 4040X, message: "..."}"
4589
+ "npm run build exits 0 with no TypeScript errors"
4590
+ "Prisma schema has Task model with fields: id, title, status, createdAt"
4591
+ "Store action createTask sets loading:true during request, loading:false on completion"
4592
+ "Route /tasks renders TaskList component, visible in router DevTools"
4593
+
4594
+ Bad examples (too vague \u2014 do NOT use these):
4595
+ "The endpoint works correctly" \u2717
4596
+ "Data is saved to the database" \u2717
4597
+ "UI displays the correct data" \u2717
4598
+ "Error handling works" \u2717
4599
+
4600
+ Rules:
4601
+ - At least 2 verification steps per task, max 5
4602
+ - Each step must be independently runnable/checkable
4603
+ - Backend tasks: include at least one HTTP request/response check and one data-layer check
4604
+ - Frontend tasks: include at least one UI render check and one state check
4605
+ - Build/compile tasks: always include "npm run build exits 0" or equivalent
4606
+
4448
4607
  Other rules:
4449
- - acceptanceCriteria must be verifiable (not vague like "works correctly")
4608
+ - acceptanceCriteria: behavioral statements ("order is created with pending status") \u2014 complementary to verificationSteps, not duplicates
4450
4609
  - dependencies must reflect real implementation order
4451
4610
  - Aim for 4-10 tasks total \u2014 not too granular, not too coarse
4452
4611
  - Each task should be completable in one focused coding session`;
@@ -4556,6 +4715,14 @@ function printTasks(tasks) {
4556
4715
  const badge = color(`[${task.layer}]`);
4557
4716
  const prio = task.priority === "high" ? import_chalk4.default.red("\u25CF") : task.priority === "medium" ? import_chalk4.default.yellow("\u25CF") : import_chalk4.default.gray("\u25CF");
4558
4717
  console.log(` ${prio} ${import_chalk4.default.bold(task.id)} ${badge} ${task.title}`);
4718
+ if (task.verificationSteps?.length) {
4719
+ for (const step of task.verificationSteps.slice(0, 2)) {
4720
+ console.log(import_chalk4.default.gray(` \u2713 ${step}`));
4721
+ }
4722
+ if (task.verificationSteps.length > 2) {
4723
+ console.log(import_chalk4.default.gray(` + ${task.verificationSteps.length - 2} more verification step(s)`));
4724
+ }
4725
+ }
4559
4726
  }
4560
4727
  }
4561
4728
  async function loadTasksForSpec(specFilePath) {
@@ -4634,6 +4801,21 @@ function validateDsl(raw) {
4634
4801
  for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
4635
4802
  validateEndpoint(eps[i], `endpoints[${i}]`, errors);
4636
4803
  }
4804
+ const seenEpIds = /* @__PURE__ */ new Set();
4805
+ for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
4806
+ const ep = eps[i];
4807
+ if (ep && typeof ep === "object" && typeof ep["id"] === "string") {
4808
+ const id = ep["id"];
4809
+ if (seenEpIds.has(id)) {
4810
+ errors.push({
4811
+ path: `endpoints[${i}].id`,
4812
+ message: `Duplicate endpoint id "${id}" \u2014 each endpoint must have a unique id`
4813
+ });
4814
+ } else {
4815
+ seenEpIds.add(id);
4816
+ }
4817
+ }
4818
+ }
4637
4819
  }
4638
4820
  if (obj["behaviors"] !== void 0) {
4639
4821
  if (!Array.isArray(obj["behaviors"])) {
@@ -4690,6 +4872,21 @@ function validateModel(raw, path10, errors) {
4690
4872
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4691
4873
  validateModelField(fields[j2], `${path10}.fields[${j2}]`, errors);
4692
4874
  }
4875
+ const seenFieldNames = /* @__PURE__ */ new Set();
4876
+ for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4877
+ const f = fields[j2];
4878
+ if (f && typeof f === "object" && typeof f["name"] === "string") {
4879
+ const name = f["name"];
4880
+ if (seenFieldNames.has(name)) {
4881
+ errors.push({
4882
+ path: `${path10}.fields[${j2}].name`,
4883
+ message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
4884
+ });
4885
+ } else {
4886
+ seenFieldNames.add(name);
4887
+ }
4888
+ }
4889
+ }
4693
4890
  }
4694
4891
  if (m["relations"] !== void 0) {
4695
4892
  if (!Array.isArray(m["relations"])) {
@@ -5720,9 +5917,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5720
5917
  console.log(import_chalk8.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5721
5918
  console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
5722
5919
  try {
5723
- (0, import_child_process.execSync)(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
5920
+ (0, import_child_process.spawnSync)(claudeCmd, ["-p", promptContent], {
5724
5921
  cwd: workingDir,
5725
- stdio: "inherit"
5922
+ stdio: "inherit",
5923
+ shell: false
5726
5924
  });
5727
5925
  console.log(import_chalk8.default.green("\n \u2714 Claude Code completed."));
5728
5926
  } catch {
@@ -5774,9 +5972,10 @@ Full spec is at: ${specFilePath}
5774
5972
  Implement ONLY this task. Do not implement other tasks.`;
5775
5973
  let taskStatus = "done";
5776
5974
  try {
5777
- (0, import_child_process.execSync)(`${claudeCmd} -p "${taskPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`, {
5975
+ (0, import_child_process.spawnSync)(claudeCmd, ["-p", taskPrompt], {
5778
5976
  cwd: workingDir,
5779
- stdio: "inherit"
5977
+ stdio: "inherit",
5978
+ shell: false
5780
5979
  });
5781
5980
  completed++;
5782
5981
  } catch {
@@ -6179,27 +6378,202 @@ function printTaskProgress(completed, total, task, mode) {
6179
6378
  }
6180
6379
 
6181
6380
  // core/reviewer.ts
6182
- var import_chalk9 = __toESM(require("chalk"));
6381
+ var import_chalk10 = __toESM(require("chalk"));
6183
6382
  var import_child_process2 = require("child_process");
6184
- var path7 = __toESM(require("path"));
6383
+ var path8 = __toESM(require("path"));
6384
+ var fs12 = __toESM(require("fs-extra"));
6385
+
6386
+ // core/constitution-generator.ts
6387
+ var import_chalk9 = __toESM(require("chalk"));
6185
6388
  var fs11 = __toESM(require("fs-extra"));
6389
+ var path7 = __toESM(require("path"));
6390
+
6391
+ // prompts/constitution.prompt.ts
6392
+ var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
6393
+
6394
+ Output a Markdown document with EXACTLY these sections. Be specific and derive rules directly from the observed codebase \u2014 no generic advice.
6395
+
6396
+ ---
6397
+
6398
+ # Project Constitution
6399
+
6400
+ ## 1. \u67B6\u6784\u89C4\u5219 (Architecture Rules)
6401
+ \u5217\u51FA\u9879\u76EE\u7684\u6838\u5FC3\u67B6\u6784\u6A21\u5F0F\u548C\u5F3A\u5236\u7EA6\u675F\uFF08\u4ECE\u4EE3\u7801\u4E2D\u63D0\u53D6\uFF0C\u800C\u975E\u901A\u7528\u5EFA\u8BAE\uFF09\u3002
6402
+ - \u5206\u5C42\u67B6\u6784\u89C4\u5219\uFF08\u5982\uFF1Aroutes \u2192 controllers \u2192 services \u2192 DB\uFF09
6403
+ - \u7981\u6B62\u8DE8\u5C42\u76F4\u63A5\u8C03\u7528\u7684\u89C4\u5219
6404
+ - \u6A21\u5757\u7EC4\u7EC7\u89C4\u8303
6405
+
6406
+ ## 2. \u547D\u540D\u89C4\u8303 (Naming Conventions)
6407
+ - \u6587\u4EF6\u547D\u540D\u89C4\u5219\uFF08\u9A7C\u5CF0/\u4E0B\u5212\u7EBF/kebab\uFF09
6408
+ - \u53D8\u91CF\u3001\u51FD\u6570\u3001\u7C7B\u7684\u547D\u540D\u6A21\u5F0F
6409
+ - \u8DEF\u7531\u8DEF\u5F84\u7684\u547D\u540D\u89C4\u8303
6410
+
6411
+ ## 3. API \u89C4\u8303 (API Patterns)
6412
+ - \u8DEF\u7531\u524D\u7F00\u89C4\u5219\uFF08\u5982 /api/v1/client/... vs /api/v1/admin/...\uFF09
6413
+ - \u7EDF\u4E00\u54CD\u5E94\u7ED3\u6784\u6A21\u677F\uFF08code/message/data \u683C\u5F0F\uFF09
6414
+ - \u9519\u8BEF\u7801\u89C4\u8303\uFF08\u5DF2\u6709\u7684\u9519\u8BEF\u7801\u8303\u56F4\u548C\u542B\u4E49\uFF09
6415
+ - \u8BA4\u8BC1/\u9274\u6743\u6A21\u5F0F\uFF08middleware \u540D\u79F0\u548C\u4F7F\u7528\u4F4D\u7F6E\uFF09
6416
+
6417
+ ## 4. \u6570\u636E\u5C42\u89C4\u8303 (Data Layer Rules)
6418
+ - ORM/\u6570\u636E\u5E93\u8BBF\u95EE\u89C4\u5219\uFF08\u4EC5\u901A\u8FC7 service \u5C42\u8BBF\u95EE\uFF1F\u76F4\u63A5\u7528 Prisma/Mongoose\uFF1F\uFF09
6419
+ - \u5DF2\u6709\u7684\u6570\u636E\u6A21\u578B\u547D\u540D\u89C4\u8303
6420
+ - \u4E8B\u52A1\u5904\u7406\u6A21\u5F0F
6421
+
6422
+ ## 5. \u9519\u8BEF\u5904\u7406\u89C4\u8303 (Error Handling Patterns)
6423
+ - \u7EDF\u4E00\u9519\u8BEF\u5904\u7406 middleware \u7684\u4F7F\u7528\u89C4\u5219
6424
+ - \u9519\u8BEF\u629B\u51FA\u548C\u6355\u83B7\u7684\u6A21\u5F0F
6425
+ - \u5DF2\u77E5\u9519\u8BEF\u7801\u5217\u8868\uFF08\u4ECE\u4EE3\u7801\u4E2D\u63D0\u53D6\uFF09
6426
+
6427
+ ## 6. \u7981\u533A (Red Lines \u2014 Never Violate)
6428
+ \u660E\u786E\u5217\u51FA\u7EDD\u5BF9\u4E0D\u80FD\u505A\u7684\u4E8B\u60C5\uFF08\u4ECE\u73B0\u6709\u4EE3\u7801/\u67B6\u6784\u63A8\u65AD\uFF09\uFF1A
6429
+ - [ ] \u7981\u6B62 ...
6430
+ - [ ] \u7981\u6B62 ...
6431
+
6432
+ ## 7. \u6D4B\u8BD5\u89C4\u8303 (Testing Rules)
6433
+ - \u6D4B\u8BD5\u6587\u4EF6\u5B58\u653E\u4F4D\u7F6E
6434
+ - \u5FC5\u987B\u8986\u76D6\u7684\u6D4B\u8BD5\u573A\u666F\u7C7B\u578B
6435
+ - \u6D4B\u8BD5\u6846\u67B6\u548C\u5DE5\u5177
6436
+
6437
+ ## 8. \u5171\u4EAB\u914D\u7F6E\u6587\u4EF6\u6E05\u5355 (Shared Config Files \u2014 Append-Only)
6438
+
6439
+ CRITICAL: The following files are **singleton config files** that already exist in the project.
6440
+ When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
6441
+ appended/merged into these existing files. **NEVER create a new parallel file.**
6442
+
6443
+ For each discovered file, list it as:
6444
+ - \`<relative-path>\` \u2014 <category> \u2014 **MODIFY ONLY, never recreate**
6445
+
6446
+ If the project context includes i18n/locale files: list ALL of them with their paths.
6447
+ If the project context includes constants/enums files: list ALL of them.
6448
+ If the project context includes route index files: list ALL of them.
6449
+ If none are provided in the context, write: "(No shared config files detected \u2014 will be populated on first run)"
6450
+
6451
+ ---
6452
+
6453
+ Be concise. Each rule must be specific enough to enforce, not a vague principle.
6454
+ **Section 8 is the most important section for preventing file duplication bugs.**`;
6455
+
6456
+ // core/constitution-generator.ts
6457
+ var CONSTITUTION_FILE = ".ai-spec-constitution.md";
6458
+ var ConstitutionGenerator = class {
6459
+ constructor(provider) {
6460
+ this.provider = provider;
6461
+ }
6462
+ async generate(projectRoot) {
6463
+ const loader = new ContextLoader(projectRoot);
6464
+ const context = await loader.loadProjectContext();
6465
+ const prompt = buildConstitutionPrompt(context, projectRoot);
6466
+ return this.provider.generate(prompt, constitutionSystemPrompt);
6467
+ }
6468
+ async saveConstitution(projectRoot, content) {
6469
+ const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6470
+ await fs11.writeFile(filePath, content, "utf-8");
6471
+ return filePath;
6472
+ }
6473
+ };
6474
+ function buildConstitutionPrompt(context, projectRoot) {
6475
+ const parts = [
6476
+ "Analyze this project and generate its Project Constitution.\n",
6477
+ `=== Tech Stack ===
6478
+ ${context.techStack.join(", ") || "unknown"}
6479
+ `,
6480
+ `=== Dependencies (top 30) ===
6481
+ ${context.dependencies.slice(0, 30).join(", ")}
6482
+ `
6483
+ ];
6484
+ if (context.apiStructure.length > 0) {
6485
+ parts.push(`=== API/Route Files ===
6486
+ ${context.apiStructure.join("\n")}
6487
+ `);
6488
+ }
6489
+ if (context.routeSummary) {
6490
+ parts.push(`=== Route Code Samples ===
6491
+ ${context.routeSummary}
6492
+ `);
6493
+ }
6494
+ if (context.schema) {
6495
+ parts.push(`=== Prisma Schema ===
6496
+ ${context.schema.slice(0, 4e3)}
6497
+ `);
6498
+ }
6499
+ if (context.errorPatterns) {
6500
+ parts.push(`=== Error Handling Patterns ===
6501
+ ${context.errorPatterns}
6502
+ `);
6503
+ }
6504
+ if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
6505
+ const grouped = context.sharedConfigFiles.reduce(
6506
+ (acc, f) => {
6507
+ (acc[f.category] ??= []).push(f);
6508
+ return acc;
6509
+ },
6510
+ {}
6511
+ );
6512
+ const sections = [];
6513
+ for (const [category, files] of Object.entries(grouped)) {
6514
+ sections.push(`--- ${category} ---`);
6515
+ for (const f of files) {
6516
+ sections.push(`File: ${f.path}
6517
+ ${f.preview.slice(0, 600)}
6518
+ `);
6519
+ }
6520
+ }
6521
+ parts.push(`=== Existing Shared Config Files (Append-Only \u2014 NEVER Recreate) ===
6522
+ ${sections.join("\n")}
6523
+ `);
6524
+ }
6525
+ return parts.join("\n");
6526
+ }
6527
+ async function loadConstitution(projectRoot) {
6528
+ const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6529
+ if (await fs11.pathExists(filePath)) {
6530
+ return fs11.readFile(filePath, "utf-8");
6531
+ }
6532
+ return void 0;
6533
+ }
6534
+ function printConstitutionHint(exists) {
6535
+ if (!exists) {
6536
+ console.log(
6537
+ import_chalk9.default.yellow(
6538
+ " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6539
+ )
6540
+ );
6541
+ }
6542
+ }
6543
+
6544
+ // core/reviewer.ts
6545
+ async function loadAccumulatedLessons(projectRoot) {
6546
+ const constitutionPath = path8.join(projectRoot, CONSTITUTION_FILE);
6547
+ let content;
6548
+ try {
6549
+ content = await fs12.readFile(constitutionPath, "utf-8");
6550
+ } catch {
6551
+ return null;
6552
+ }
6553
+ const marker = "## 9. \u79EF\u7D2F\u6559\u8BAD";
6554
+ const idx = content.indexOf(marker);
6555
+ if (idx === -1) return null;
6556
+ const section = content.slice(idx);
6557
+ const nextSection = section.slice(marker.length).match(/\n## \d/);
6558
+ return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
6559
+ }
6186
6560
  var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
6187
6561
  async function loadReviewHistory(projectRoot) {
6188
- const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
6562
+ const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6189
6563
  try {
6190
- if (await fs11.pathExists(historyPath)) {
6191
- return await fs11.readJson(historyPath);
6564
+ if (await fs12.pathExists(historyPath)) {
6565
+ return await fs12.readJson(historyPath);
6192
6566
  }
6193
6567
  } catch {
6194
6568
  }
6195
6569
  return [];
6196
6570
  }
6197
6571
  async function appendReviewHistory(projectRoot, entry) {
6198
- const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
6572
+ const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6199
6573
  const existing = await loadReviewHistory(projectRoot);
6200
6574
  const updated = [...existing, entry].slice(-20);
6201
6575
  try {
6202
- await fs11.writeJson(historyPath, updated, { spaces: 2 });
6576
+ await fs12.writeJson(historyPath, updated, { spaces: 2 });
6203
6577
  } catch {
6204
6578
  }
6205
6579
  }
@@ -6207,6 +6581,14 @@ function extractScore(reviewText) {
6207
6581
  const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
6208
6582
  return match ? parseFloat(match[1]) : 0;
6209
6583
  }
6584
+ function extractComplianceScore(complianceText) {
6585
+ const match = complianceText.match(/ComplianceScore:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
6586
+ return match ? parseFloat(match[1]) : 0;
6587
+ }
6588
+ function extractMissingCount(complianceText) {
6589
+ const summaryMatch = complianceText.match(/Missing:\s*(\d+)/i);
6590
+ return summaryMatch ? parseInt(summaryMatch[1], 10) : 0;
6591
+ }
6210
6592
  function extractImpactLevel(reviewText) {
6211
6593
  const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
6212
6594
  return match ? match[1] : void 0;
@@ -6225,7 +6607,7 @@ function buildHistoryContext(history) {
6225
6607
  const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
6226
6608
  for (const entry of recent) {
6227
6609
  lines.push(`
6228
- [${entry.date}] ${path7.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6610
+ [${entry.date}] ${path8.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6229
6611
  entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
6230
6612
  }
6231
6613
  return lines.join("\n") + "\n";
@@ -6260,23 +6642,52 @@ var CodeReviewer = class {
6260
6642
  };
6261
6643
  }
6262
6644
  /**
6263
- * Three-pass review:
6264
- * Pass 1architecture (spec compliance, layer separation, auth)
6645
+ * Four-pass review:
6646
+ * Pass 0 — spec compliance (exhaustive requirement coverage audit)
6647
+ * Pass 1 — architecture (layer separation, contract design, auth posture)
6265
6648
  * Pass 2 — implementation details (validation, error handling, edge cases)
6266
6649
  * + historical issue recurrence check
6267
6650
  * Pass 3 — impact assessment + code complexity
6268
6651
  */
6269
6652
  async runThreePassReview(specContent, codeContext, specFile) {
6270
- console.log(import_chalk9.default.gray(" Pass 1/3: Architecture review..."));
6271
- const archPrompt = `Review the architecture of this change.
6653
+ let complianceReview = "";
6654
+ if (specContent && specContent.trim() && specContent !== "(No spec \u2014 review for general code quality)") {
6655
+ console.log(import_chalk10.default.gray(" Pass 0/3: Spec compliance check..."));
6656
+ const compliancePrompt = `Check whether the implementation covers every requirement in the spec.
6272
6657
 
6658
+ === Feature Spec ===
6659
+ ${specContent}
6660
+
6661
+ === Code ===
6662
+ ${codeContext}`;
6663
+ complianceReview = await this.provider.generate(compliancePrompt, specComplianceSystemPrompt);
6664
+ const complianceScore2 = extractComplianceScore(complianceReview);
6665
+ const missingCount = extractMissingCount(complianceReview);
6666
+ if (complianceScore2 > 0) {
6667
+ const scoreColor = complianceScore2 >= 8 ? import_chalk10.default.green : complianceScore2 >= 6 ? import_chalk10.default.yellow : import_chalk10.default.red;
6668
+ console.log(
6669
+ import_chalk10.default.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? import_chalk10.default.red(` \xB7 ${missingCount} missing requirement(s)`) : import_chalk10.default.green(" \xB7 all requirements covered"))
6670
+ );
6671
+ }
6672
+ }
6673
+ console.log(import_chalk10.default.gray(` Pass 1/3: Architecture review...`));
6674
+ const accumulatedLessons = await loadAccumulatedLessons(this.projectRoot);
6675
+ const archPrompt = `Review the architecture of this change.
6676
+ ${complianceReview ? `
6677
+ === Spec Compliance Report (Pass 0 \u2014 already audited, do NOT re-audit missing requirements) ===
6678
+ ${complianceReview}
6679
+ ` : ""}
6680
+ ${accumulatedLessons ? `
6681
+ === \xA79 \u5386\u53F2\u79EF\u7D2F\u6559\u8BAD (Accumulated Lessons \u2014 check if any are repeated in this code) ===
6682
+ ${accumulatedLessons}
6683
+ ` : ""}
6273
6684
  === Feature Spec ===
6274
6685
  ${specContent || "(No spec \u2014 review for general code quality)"}
6275
6686
 
6276
6687
  === Code ===
6277
6688
  ${codeContext}`;
6278
6689
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6279
- console.log(import_chalk9.default.gray(" Pass 2/3: Implementation review..."));
6690
+ console.log(import_chalk10.default.gray(" Pass 2/3: Implementation review..."));
6280
6691
  const history = await loadReviewHistory(this.projectRoot);
6281
6692
  const historyContext = buildHistoryContext(history);
6282
6693
  const implPrompt = `Review the implementation details of this change.
@@ -6291,7 +6702,7 @@ ${codeContext}
6291
6702
  ${archReview}
6292
6703
  ${historyContext}`;
6293
6704
  const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
6294
- console.log(import_chalk9.default.gray(" Pass 3/3: Impact & complexity assessment..."));
6705
+ console.log(import_chalk10.default.gray(" Pass 3/3: Impact & complexity assessment..."));
6295
6706
  const impactPrompt = `Assess the impact and complexity of this change.
6296
6707
 
6297
6708
  === Feature Spec ===
@@ -6307,24 +6718,23 @@ ${archReview}
6307
6718
  ${implReview}`;
6308
6719
  const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
6309
6720
  const sep = "\u2500".repeat(52);
6310
- const combined = `${archReview}
6721
+ const parts = complianceReview ? [complianceReview, archReview, implReview, impactReview] : [archReview, implReview, impactReview];
6722
+ const combined = parts.join(`
6311
6723
 
6312
6724
  ${sep}
6313
6725
 
6314
- ${implReview}
6315
-
6316
- ${sep}
6317
-
6318
- ${impactReview}`;
6726
+ `);
6319
6727
  const score = extractScore(implReview) || extractScore(archReview);
6728
+ const complianceScore = extractComplianceScore(complianceReview);
6320
6729
  const topIssues = extractTopIssues(implReview);
6321
6730
  const impactLevel = extractImpactLevel(impactReview);
6322
6731
  const complexityLevel = extractComplexityLevel(impactReview);
6323
6732
  if (score > 0 && specFile) {
6324
6733
  await appendReviewHistory(this.projectRoot, {
6325
6734
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
6326
- specFile: path7.relative(this.projectRoot, specFile),
6735
+ specFile: path8.relative(this.projectRoot, specFile),
6327
6736
  score,
6737
+ ...complianceScore > 0 ? { complianceScore } : {},
6328
6738
  topIssues,
6329
6739
  ...impactLevel ? { impactLevel } : {},
6330
6740
  ...complexityLevel ? { complexityLevel } : {}
@@ -6333,43 +6743,43 @@ ${impactReview}`;
6333
6743
  return combined;
6334
6744
  }
6335
6745
  async reviewCode(specContent, specFile) {
6336
- console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6746
+ console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Automated Code Review \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6337
6747
  const diff = this.getGitDiff();
6338
6748
  if (!diff.trim()) {
6339
6749
  console.log(
6340
- import_chalk9.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
6750
+ import_chalk10.default.yellow(" No git diff found. Stage or commit changes first, then run review.")
6341
6751
  );
6342
- console.log(import_chalk9.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6752
+ console.log(import_chalk10.default.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6343
6753
  return "No changes";
6344
6754
  }
6345
6755
  const { files, added, removed } = this.getDiffStats(diff);
6346
6756
  console.log(
6347
- import_chalk9.default.gray(` Diff: ${files} file(s), ${import_chalk9.default.green("+" + added)} ${import_chalk9.default.red("-" + removed)}`)
6757
+ import_chalk10.default.gray(` Diff: ${files} file(s), ${import_chalk10.default.green("+" + added)} ${import_chalk10.default.red("-" + removed)}`)
6348
6758
  );
6349
6759
  console.log(
6350
- import_chalk9.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6760
+ import_chalk10.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6351
6761
  );
6352
6762
  const codeContext = diff.slice(0, 1e4);
6353
6763
  const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
6354
- console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6764
+ console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6355
6765
  console.log(reviewResult);
6356
- console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6766
+ console.log(import_chalk10.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6357
6767
  return reviewResult;
6358
6768
  }
6359
6769
  /**
6360
6770
  * Review directly from generated file contents (for api mode where git diff is empty).
6361
6771
  */
6362
6772
  async reviewFiles(specContent, filePaths, workingDir, specFile) {
6363
- console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6364
- console.log(import_chalk9.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6773
+ console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6774
+ console.log(import_chalk10.default.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6365
6775
  console.log(
6366
- import_chalk9.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6776
+ import_chalk10.default.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6367
6777
  );
6368
6778
  let filesSection = "";
6369
6779
  for (const filePath of filePaths) {
6370
- const fullPath = path7.join(workingDir, filePath);
6780
+ const fullPath = path8.join(workingDir, filePath);
6371
6781
  try {
6372
- const content = await fs11.readFile(fullPath, "utf-8");
6782
+ const content = await fs12.readFile(fullPath, "utf-8");
6373
6783
  filesSection += `
6374
6784
 
6375
6785
  === ${filePath} ===
@@ -6384,189 +6794,31 @@ ${content.slice(0, 3e3)}`;
6384
6794
  }
6385
6795
  }
6386
6796
  const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
6387
- console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6797
+ console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Review Result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6388
6798
  console.log(reviewResult);
6389
- console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6799
+ console.log(import_chalk10.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
6390
6800
  return reviewResult;
6391
6801
  }
6392
6802
  /** Print score trend from history (last N reviews) */
6393
6803
  async printScoreTrend(limit = 5) {
6394
6804
  const history = await loadReviewHistory(this.projectRoot);
6395
6805
  if (history.length === 0) {
6396
- console.log(import_chalk9.default.gray(" No review history yet."));
6806
+ console.log(import_chalk10.default.gray(" No review history yet."));
6397
6807
  return;
6398
6808
  }
6399
6809
  const recent = history.slice(-limit);
6400
- console.log(import_chalk9.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6810
+ console.log(import_chalk10.default.cyan("\n\u2500\u2500\u2500 Review Score Trend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6401
6811
  for (const entry of recent) {
6402
6812
  const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
6403
- const color = entry.score >= 8 ? import_chalk9.default.green : entry.score >= 6 ? import_chalk9.default.yellow : import_chalk9.default.red;
6404
- const impactTag = entry.impactLevel ? import_chalk9.default.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? import_chalk9.default.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? import_chalk9.default.yellow(entry.impactLevel) : import_chalk9.default.green(entry.impactLevel)}`) : "";
6405
- const complexityTag = entry.complexityLevel ? import_chalk9.default.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? import_chalk9.default.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? import_chalk9.default.yellow(entry.complexityLevel) : import_chalk9.default.green(entry.complexityLevel)}`) : "";
6406
- console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path7.basename(entry.specFile)}`);
6813
+ const color = entry.score >= 8 ? import_chalk10.default.green : entry.score >= 6 ? import_chalk10.default.yellow : import_chalk10.default.red;
6814
+ const impactTag = entry.impactLevel ? import_chalk10.default.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? import_chalk10.default.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? import_chalk10.default.yellow(entry.impactLevel) : import_chalk10.default.green(entry.impactLevel)}`) : "";
6815
+ const complexityTag = entry.complexityLevel ? import_chalk10.default.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? import_chalk10.default.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? import_chalk10.default.yellow(entry.complexityLevel) : import_chalk10.default.green(entry.complexityLevel)}`) : "";
6816
+ console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
6407
6817
  }
6408
- console.log(import_chalk9.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6818
+ console.log(import_chalk10.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6409
6819
  }
6410
6820
  };
6411
6821
 
6412
- // core/constitution-generator.ts
6413
- var import_chalk10 = __toESM(require("chalk"));
6414
- var fs12 = __toESM(require("fs-extra"));
6415
- var path8 = __toESM(require("path"));
6416
-
6417
- // prompts/constitution.prompt.ts
6418
- var constitutionSystemPrompt = `You are a Senior Software Architect. Analyze the provided project codebase context and generate a concise "Project Constitution" \u2014 a living document that captures the architectural rules, conventions, and red lines that ALL future feature specs and code generation MUST follow.
6419
-
6420
- Output a Markdown document with EXACTLY these sections. Be specific and derive rules directly from the observed codebase \u2014 no generic advice.
6421
-
6422
- ---
6423
-
6424
- # Project Constitution
6425
-
6426
- ## 1. \u67B6\u6784\u89C4\u5219 (Architecture Rules)
6427
- \u5217\u51FA\u9879\u76EE\u7684\u6838\u5FC3\u67B6\u6784\u6A21\u5F0F\u548C\u5F3A\u5236\u7EA6\u675F\uFF08\u4ECE\u4EE3\u7801\u4E2D\u63D0\u53D6\uFF0C\u800C\u975E\u901A\u7528\u5EFA\u8BAE\uFF09\u3002
6428
- - \u5206\u5C42\u67B6\u6784\u89C4\u5219\uFF08\u5982\uFF1Aroutes \u2192 controllers \u2192 services \u2192 DB\uFF09
6429
- - \u7981\u6B62\u8DE8\u5C42\u76F4\u63A5\u8C03\u7528\u7684\u89C4\u5219
6430
- - \u6A21\u5757\u7EC4\u7EC7\u89C4\u8303
6431
-
6432
- ## 2. \u547D\u540D\u89C4\u8303 (Naming Conventions)
6433
- - \u6587\u4EF6\u547D\u540D\u89C4\u5219\uFF08\u9A7C\u5CF0/\u4E0B\u5212\u7EBF/kebab\uFF09
6434
- - \u53D8\u91CF\u3001\u51FD\u6570\u3001\u7C7B\u7684\u547D\u540D\u6A21\u5F0F
6435
- - \u8DEF\u7531\u8DEF\u5F84\u7684\u547D\u540D\u89C4\u8303
6436
-
6437
- ## 3. API \u89C4\u8303 (API Patterns)
6438
- - \u8DEF\u7531\u524D\u7F00\u89C4\u5219\uFF08\u5982 /api/v1/client/... vs /api/v1/admin/...\uFF09
6439
- - \u7EDF\u4E00\u54CD\u5E94\u7ED3\u6784\u6A21\u677F\uFF08code/message/data \u683C\u5F0F\uFF09
6440
- - \u9519\u8BEF\u7801\u89C4\u8303\uFF08\u5DF2\u6709\u7684\u9519\u8BEF\u7801\u8303\u56F4\u548C\u542B\u4E49\uFF09
6441
- - \u8BA4\u8BC1/\u9274\u6743\u6A21\u5F0F\uFF08middleware \u540D\u79F0\u548C\u4F7F\u7528\u4F4D\u7F6E\uFF09
6442
-
6443
- ## 4. \u6570\u636E\u5C42\u89C4\u8303 (Data Layer Rules)
6444
- - ORM/\u6570\u636E\u5E93\u8BBF\u95EE\u89C4\u5219\uFF08\u4EC5\u901A\u8FC7 service \u5C42\u8BBF\u95EE\uFF1F\u76F4\u63A5\u7528 Prisma/Mongoose\uFF1F\uFF09
6445
- - \u5DF2\u6709\u7684\u6570\u636E\u6A21\u578B\u547D\u540D\u89C4\u8303
6446
- - \u4E8B\u52A1\u5904\u7406\u6A21\u5F0F
6447
-
6448
- ## 5. \u9519\u8BEF\u5904\u7406\u89C4\u8303 (Error Handling Patterns)
6449
- - \u7EDF\u4E00\u9519\u8BEF\u5904\u7406 middleware \u7684\u4F7F\u7528\u89C4\u5219
6450
- - \u9519\u8BEF\u629B\u51FA\u548C\u6355\u83B7\u7684\u6A21\u5F0F
6451
- - \u5DF2\u77E5\u9519\u8BEF\u7801\u5217\u8868\uFF08\u4ECE\u4EE3\u7801\u4E2D\u63D0\u53D6\uFF09
6452
-
6453
- ## 6. \u7981\u533A (Red Lines \u2014 Never Violate)
6454
- \u660E\u786E\u5217\u51FA\u7EDD\u5BF9\u4E0D\u80FD\u505A\u7684\u4E8B\u60C5\uFF08\u4ECE\u73B0\u6709\u4EE3\u7801/\u67B6\u6784\u63A8\u65AD\uFF09\uFF1A
6455
- - [ ] \u7981\u6B62 ...
6456
- - [ ] \u7981\u6B62 ...
6457
-
6458
- ## 7. \u6D4B\u8BD5\u89C4\u8303 (Testing Rules)
6459
- - \u6D4B\u8BD5\u6587\u4EF6\u5B58\u653E\u4F4D\u7F6E
6460
- - \u5FC5\u987B\u8986\u76D6\u7684\u6D4B\u8BD5\u573A\u666F\u7C7B\u578B
6461
- - \u6D4B\u8BD5\u6846\u67B6\u548C\u5DE5\u5177
6462
-
6463
- ## 8. \u5171\u4EAB\u914D\u7F6E\u6587\u4EF6\u6E05\u5355 (Shared Config Files \u2014 Append-Only)
6464
-
6465
- CRITICAL: The following files are **singleton config files** that already exist in the project.
6466
- When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
6467
- appended/merged into these existing files. **NEVER create a new parallel file.**
6468
-
6469
- For each discovered file, list it as:
6470
- - \`<relative-path>\` \u2014 <category> \u2014 **MODIFY ONLY, never recreate**
6471
-
6472
- If the project context includes i18n/locale files: list ALL of them with their paths.
6473
- If the project context includes constants/enums files: list ALL of them.
6474
- If the project context includes route index files: list ALL of them.
6475
- If none are provided in the context, write: "(No shared config files detected \u2014 will be populated on first run)"
6476
-
6477
- ---
6478
-
6479
- Be concise. Each rule must be specific enough to enforce, not a vague principle.
6480
- **Section 8 is the most important section for preventing file duplication bugs.**`;
6481
-
6482
- // core/constitution-generator.ts
6483
- var CONSTITUTION_FILE = ".ai-spec-constitution.md";
6484
- var ConstitutionGenerator = class {
6485
- constructor(provider) {
6486
- this.provider = provider;
6487
- }
6488
- async generate(projectRoot) {
6489
- const loader = new ContextLoader(projectRoot);
6490
- const context = await loader.loadProjectContext();
6491
- const prompt = buildConstitutionPrompt(context, projectRoot);
6492
- return this.provider.generate(prompt, constitutionSystemPrompt);
6493
- }
6494
- async saveConstitution(projectRoot, content) {
6495
- const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6496
- await fs12.writeFile(filePath, content, "utf-8");
6497
- return filePath;
6498
- }
6499
- };
6500
- function buildConstitutionPrompt(context, projectRoot) {
6501
- const parts = [
6502
- "Analyze this project and generate its Project Constitution.\n",
6503
- `=== Tech Stack ===
6504
- ${context.techStack.join(", ") || "unknown"}
6505
- `,
6506
- `=== Dependencies (top 30) ===
6507
- ${context.dependencies.slice(0, 30).join(", ")}
6508
- `
6509
- ];
6510
- if (context.apiStructure.length > 0) {
6511
- parts.push(`=== API/Route Files ===
6512
- ${context.apiStructure.join("\n")}
6513
- `);
6514
- }
6515
- if (context.routeSummary) {
6516
- parts.push(`=== Route Code Samples ===
6517
- ${context.routeSummary}
6518
- `);
6519
- }
6520
- if (context.schema) {
6521
- parts.push(`=== Prisma Schema ===
6522
- ${context.schema.slice(0, 4e3)}
6523
- `);
6524
- }
6525
- if (context.errorPatterns) {
6526
- parts.push(`=== Error Handling Patterns ===
6527
- ${context.errorPatterns}
6528
- `);
6529
- }
6530
- if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
6531
- const grouped = context.sharedConfigFiles.reduce(
6532
- (acc, f) => {
6533
- (acc[f.category] ??= []).push(f);
6534
- return acc;
6535
- },
6536
- {}
6537
- );
6538
- const sections = [];
6539
- for (const [category, files] of Object.entries(grouped)) {
6540
- sections.push(`--- ${category} ---`);
6541
- for (const f of files) {
6542
- sections.push(`File: ${f.path}
6543
- ${f.preview.slice(0, 600)}
6544
- `);
6545
- }
6546
- }
6547
- parts.push(`=== Existing Shared Config Files (Append-Only \u2014 NEVER Recreate) ===
6548
- ${sections.join("\n")}
6549
- `);
6550
- }
6551
- return parts.join("\n");
6552
- }
6553
- async function loadConstitution(projectRoot) {
6554
- const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6555
- if (await fs12.pathExists(filePath)) {
6556
- return fs12.readFile(filePath, "utf-8");
6557
- }
6558
- return void 0;
6559
- }
6560
- function printConstitutionHint(exists) {
6561
- if (!exists) {
6562
- console.log(
6563
- import_chalk10.default.yellow(
6564
- " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6565
- )
6566
- );
6567
- }
6568
- }
6569
-
6570
6822
  // core/combined-generator.ts
6571
6823
  var TASKS_SEPARATOR = "---TASKS_JSON---";
6572
6824
  var tasksInstruction = `
@@ -6575,11 +6827,21 @@ var tasksInstruction = `
6575
6827
  After outputting the complete spec above, append EXACTLY this line on its own (no extra text before or after it):
6576
6828
  ${TASKS_SEPARATOR}
6577
6829
  Then output a valid JSON array of implementation tasks. Each element must have these exact fields:
6578
- {"id":"TASK-001","title":"...","description":"1-2 sentences, specific","layer":"data|infra|service|api|test","filesToTouch":["src/..."],"acceptanceCriteria":["verifiable condition"],"dependencies":[],"priority":"high|medium|low"}
6830
+ {"id":"TASK-001","title":"...","description":"1-2 sentences, specific","layer":"data|infra|service|api|test","filesToTouch":["src/..."],"acceptanceCriteria":["behavioral condition"],"verificationSteps":["concrete runnable check \u2192 expected result"],"dependencies":[],"priority":"high|medium|low"}
6831
+ verificationSteps rules: each step is a specific command or action with observable expected output (e.g. "POST /api/orders \u2192 201 {id, status:'pending'}"). At least 2 per task, max 5. Never vague.
6579
6832
  Layer order: data \u2192 infra \u2192 service \u2192 api \u2192 test. 4-10 tasks total. filesToTouch must use real paths from the project context.`;
6580
- async function generateSpecWithTasks(provider, idea, context) {
6833
+ async function generateSpecWithTasks(provider, idea, context, architectureDecision) {
6581
6834
  const contextBlock = buildTaskPrompt("", context).trim();
6582
- const fullPrompt = [idea, contextBlock].filter(Boolean).join("\n\n");
6835
+ const parts = [idea];
6836
+ if (architectureDecision) {
6837
+ parts.push(
6838
+ `
6839
+ === Architecture Decision (MUST follow this approach in the spec) ===
6840
+ ${architectureDecision}`
6841
+ );
6842
+ }
6843
+ if (contextBlock) parts.push(contextBlock);
6844
+ const fullPrompt = parts.join("\n\n");
6583
6845
  const combinedSystemPrompt = specPrompt + tasksInstruction;
6584
6846
  const raw = await provider.generate(fullPrompt, combinedSystemPrompt);
6585
6847
  return parseSpecAndTasks(raw);
@@ -6715,6 +6977,9 @@ var GitWorktreeManager = class {
6715
6977
  TaskGenerator,
6716
6978
  buildTaskPrompt,
6717
6979
  createProvider,
6980
+ extractBehavioralContract,
6981
+ extractComplianceScore,
6982
+ extractMissingCount,
6718
6983
  generateSpecWithTasks,
6719
6984
  isFrontendDeps,
6720
6985
  loadConstitution,