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.mjs CHANGED
@@ -107,7 +107,14 @@ model ExampleModel {
107
107
 
108
108
  ---
109
109
 
110
- \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`;
110
+ \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
111
+
112
+ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uFF09\uFF1A
113
+ \u5982\u679C\u9879\u76EE\u5BAA\u6CD5\u4E2D\u5305\u542B"\xA79 \u79EF\u7D2F\u6559\u8BAD (Accumulated Lessons)"\u7AE0\u8282\uFF0C\u4F60\u5FC5\u987B\uFF1A
114
+ 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
115
+ 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
116
+ 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
117
+ 4. \u5982\u65E0\u76F8\u5173\u6559\u8BAD\uFF0C\xA78 \u4E0D\u5FC5\u8FFD\u52A0\u4EFB\u4F55\u5185\u5BB9`;
111
118
 
112
119
  // core/provider-utils.ts
113
120
  import chalk from "chalk";
@@ -124,14 +131,49 @@ function classifyError(err, label) {
124
131
  const e = err;
125
132
  const status = e.status ?? e.response?.status;
126
133
  if (status === 401 || status === 403)
127
- return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
134
+ return new ProviderError(
135
+ `Auth error (${label}): API key is invalid or expired.
136
+ \u2192 Check that the correct API key is set in your environment or ~/.ai-spec-keys.json
137
+ \u2192 Run "ai-spec model" to reconfigure your provider and key`,
138
+ "auth",
139
+ err
140
+ );
128
141
  if (status === 429)
129
- return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
142
+ return new ProviderError(
143
+ `Rate limit hit (${label}): too many requests.
144
+ \u2192 Wait a few minutes and retry, or switch to a different provider/model
145
+ \u2192 Check your provider's billing dashboard for quota status`,
146
+ "rate_limit",
147
+ err
148
+ );
130
149
  if (e._timeout || e.message?.toLowerCase().includes("timed out"))
131
150
  return new ProviderError(`Request timed out (${label})`, "timeout", err);
132
151
  if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
133
- return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
134
- return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
152
+ return new ProviderError(
153
+ `Network error (${label}): ${e.message}
154
+ \u2192 Check your internet connection and proxy settings (HTTPS_PROXY)
155
+ \u2192 If behind a firewall, ensure the provider's API endpoint is reachable`,
156
+ "network",
157
+ err
158
+ );
159
+ const msg = e.message ?? "";
160
+ if (status === 404 || msg.includes("model") && (msg.includes("not found") || msg.includes("does not exist")))
161
+ return new ProviderError(
162
+ `Model not found (${label}): ${msg}
163
+ \u2192 Run "ai-spec model" to see available models for your provider
164
+ \u2192 The model name may have changed \u2014 check your provider's documentation`,
165
+ "provider",
166
+ err
167
+ );
168
+ if (msg.includes("insufficient") || msg.includes("quota") || msg.includes("balance"))
169
+ return new ProviderError(
170
+ `Quota/balance error (${label}): ${msg}
171
+ \u2192 Check your provider's billing dashboard
172
+ \u2192 Consider switching to a different provider with "ai-spec model"`,
173
+ "provider",
174
+ err
175
+ );
176
+ return new ProviderError(`Provider error (${label}): ${msg}`, "provider", err);
135
177
  }
136
178
  function isRetryable(err) {
137
179
  const e = err;
@@ -258,17 +300,23 @@ var PROVIDER_CATALOG = {
258
300
  },
259
301
  glm: {
260
302
  displayName: "\u667A\u8C31 GLM (Zhipu AI)",
261
- description: "\u667A\u8C31 AI \u2014 GLM-5 / GLM-4 series + Z1 reasoning",
303
+ description: "\u667A\u8C31 AI \u2014 GLM-5.1 / GLM-5 / GLM-4 series",
262
304
  models: [
305
+ "glm-5.1",
306
+ // GLM-5.1 — latest flagship (2026)
263
307
  "glm-5",
264
- // GLM-5 flagship (如不可用请确认最新 model ID)
265
- "glm-5-flash",
308
+ // GLM-5 premium (Max/Pro plans)
309
+ "glm-5-turbo",
310
+ // GLM-5-Turbo — fast & cost-efficient
311
+ "glm-4.7",
312
+ // GLM-4.7
313
+ "glm-4.6",
314
+ // GLM-4.6
315
+ "glm-4.5-air",
316
+ // GLM-4.5-Air — lightweight
266
317
  "glm-z1",
267
- // GLM-Z1 reasoning model
268
- "glm-z1-flash",
269
- "glm-4-plus",
270
- "glm-4-flash",
271
- "glm-4-long"
318
+ // GLM-Z1 reasoning model
319
+ "glm-z1-flash"
272
320
  ],
273
321
  envKey: "ZHIPU_API_KEY",
274
322
  baseURL: "https://open.bigmodel.cn/api/paas/v4/"
@@ -464,8 +512,15 @@ var SpecGenerator = class {
464
512
  constructor(provider) {
465
513
  this.provider = provider;
466
514
  }
467
- async generateSpec(idea, context) {
515
+ async generateSpec(idea, context, architectureDecision) {
468
516
  const parts = [idea];
517
+ if (architectureDecision) {
518
+ parts.push(
519
+ `
520
+ === Architecture Decision (MUST follow this approach in the spec) ===
521
+ ${architectureDecision}`
522
+ );
523
+ }
469
524
  if (context) {
470
525
  if (context.constitution) {
471
526
  parts.push(
@@ -4053,7 +4108,7 @@ ${currentSpec}`,
4053
4108
 
4054
4109
  // core/code-generator.ts
4055
4110
  import chalk8 from "chalk";
4056
- import { execSync } from "child_process";
4111
+ import { execSync, spawnSync } from "child_process";
4057
4112
  import * as path6 from "path";
4058
4113
  import * as fs10 from "fs-extra";
4059
4114
 
@@ -4220,34 +4275,110 @@ function getCodeGenSystemPrompt(repoType) {
4220
4275
  return codeGenSystemPrompt;
4221
4276
  }
4222
4277
  }
4223
- var reviewArchitectureSystemPrompt = `You are a Senior Software Architect reviewing the HIGH-LEVEL design of a code change.
4278
+ var specComplianceSystemPrompt = `You are a QA Engineer performing a SPEC COMPLIANCE CHECK.
4279
+
4280
+ Your sole job is to verify that the implementation covers every requirement stated in the feature spec.
4281
+ This is a completeness audit \u2014 NOT a code quality review. Do not comment on architecture, style, or implementation details.
4282
+
4283
+ ## How to audit:
4284
+
4285
+ 1. Parse the spec and extract EVERY stated requirement into these categories:
4286
+ - **Endpoints**: each HTTP method + path listed or implied
4287
+ - **Data Models**: each entity, field, constraint mentioned
4288
+ - **Business Rules**: validations, conditions, calculations stated
4289
+ - **Auth Requirements**: which endpoints need auth, which roles are allowed
4290
+ - **Error Cases**: explicit error codes or failure scenarios mentioned
4291
+ - **Side Effects**: emails sent, events fired, caches invalidated, etc.
4292
+
4293
+ 2. For each extracted requirement, check the provided code:
4294
+ - \u2705 **Covered** \u2014 clearly implemented
4295
+ - \u26A0\uFE0F **Partial** \u2014 exists but incomplete (e.g. endpoint exists but missing a field or error case)
4296
+ - \u274C **Missing** \u2014 requirement stated in spec but not found in code
4297
+
4298
+ 3. Output a compliance checklist. Be exhaustive \u2014 list every single requirement.
4299
+
4300
+ ## Output format:
4301
+
4302
+ ## \u{1F4CB} Spec Compliance Report
4303
+
4304
+ ### Endpoints
4305
+ \u2705 / \u26A0\uFE0F / \u274C METHOD /path \u2014 one-line status
4306
+
4307
+ ### Data Models
4308
+ \u2705 / \u26A0\uFE0F / \u274C ModelName \u2014 one-line status
4309
+
4310
+ ### Business Rules
4311
+ \u2705 / \u26A0\uFE0F / \u274C Rule description \u2014 one-line status
4312
+
4313
+ ### Auth & Permissions
4314
+ \u2705 / \u26A0\uFE0F / \u274C Requirement \u2014 one-line status
4315
+
4316
+ ### Error Cases
4317
+ \u2705 / \u26A0\uFE0F / \u274C Error scenario \u2014 one-line status
4318
+
4319
+ ### Side Effects
4320
+ \u2705 / \u26A0\uFE0F / \u274C Side effect \u2014 one-line status (omit section if none in spec)
4321
+
4322
+ ---
4323
+
4324
+ ## \u{1F4CA} Compliance Summary
4325
+ Covered: N | Partial: N | Missing: N | Total: N
4326
+
4327
+ ## \u{1F522} Compliance Score
4328
+ ComplianceScore: X/10
4329
+
4330
+ (10 = all requirements implemented, 0 = nothing implemented.
4331
+ Deduct 1 point per missing requirement, 0.5 per partial.
4332
+ Round to nearest integer.)
4333
+
4334
+ ## \u{1F6A8} Blockers (Missing requirements that MUST be implemented before ship)
4335
+ List only \u274C Missing items here, ordered by severity. If none, write "None".
4336
+
4337
+ ---
4338
+
4339
+ IMPORTANT: Be exhaustive. A requirement not listed here is assumed to be covered.
4340
+ If the spec is vague, note the ambiguity as \u26A0\uFE0F Partial rather than assuming coverage.`;
4341
+ var reviewArchitectureSystemPrompt = `You are a Senior Software Architect reviewing the HIGH-LEVEL DESIGN of a code change.
4342
+
4343
+ 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.
4224
4344
 
4225
4345
  Focus ONLY on:
4226
- 1. **Spec compliance** \u2014 Does the implementation match the spec? Are there missing or extra endpoints/components?
4227
- 2. **Layer separation** \u2014 Does each layer have the right responsibilities? (e.g., no business logic in controllers, no HTTP in stores)
4228
- 3. **API contract** \u2014 Are request/response shapes correct? Are all error codes from the spec implemented?
4229
- 4. **Data model integrity** \u2014 Are constraints, unique fields, and relationships correct?
4230
- 5. **Security posture** \u2014 Are auth checks applied to the right endpoints? Any obvious missing auth?
4346
+ 1. **Layer separation** \u2014 Does each layer have the right responsibilities? (e.g., no business logic in controllers, no HTTP in stores)
4347
+ 2. **API contract quality** \u2014 Are request/response shapes well-designed? Are error codes consistent with project conventions?
4348
+ 3. **Data model integrity** \u2014 Are constraints, unique fields, and relationships modelled correctly?
4349
+ 4. **Security posture** \u2014 Are auth checks applied correctly? Any privilege escalation risks?
4231
4350
 
4232
4351
  DO NOT comment on:
4352
+ - Whether specific endpoints or features are missing (covered by Pass 0)
4233
4353
  - Code style, naming conventions, formatting
4234
4354
  - Minor implementation details (variable names, inline comments)
4235
4355
  - Performance micro-optimizations
4236
4356
 
4237
4357
  Format:
4238
4358
 
4239
- ## \u{1F3D7} \u67B6\u6784\u5408\u89C4\u6027 (Spec Compliance)
4240
- Does the implementation match the spec? List any missing or wrong endpoints/components.
4241
-
4242
4359
  ## \u{1F500} \u5C42\u804C\u8D23\u5206\u79BB (Layer Separation)
4243
4360
  Any layer boundary violations?
4244
4361
 
4245
4362
  ## \u{1F512} \u5B89\u5168\u4E0E\u6743\u9650 (Security & Auth)
4246
4363
  Any missing auth checks, exposed data, or privilege issues?
4247
4364
 
4365
+ ## \u{1F4D0} \u5951\u7EA6\u4E0E\u6A21\u578B\u8BBE\u8BA1 (Contract & Model Design)
4366
+ Response shape issues, missing constraints, relationship problems.
4367
+
4248
4368
  ## \u{1F4CB} \u67B6\u6784\u8BC4\u5206 (Architecture Score)
4249
4369
  Score: X/10 \u2014 One short paragraph.
4250
4370
 
4371
+ ## \u{1F50D} \u7ED3\u6784\u6027\u53D1\u73B0 JSON (Structural Findings \u2014 for pipeline processing)
4372
+ Output a JSON block with any design-level issues found above.
4373
+ Categories: "auth_design" | "api_contract" | "model_design" | "layer_violation" | "other_design"
4374
+ If no findings, output an empty array.
4375
+
4376
+ \`\`\`json
4377
+ {"structuralFindings": [{"category": "...", "description": "one sentence referencing the specific endpoint/model/file"}]}
4378
+ \`\`\`
4379
+
4380
+ IMPORTANT: Always include this JSON block, even when structuralFindings is []. This block is parsed by the pipeline.
4381
+
4251
4382
  Be specific. Reference file names or endpoint paths.`;
4252
4383
  var reviewImplementationSystemPrompt = `You are a Senior Engineer reviewing the IMPLEMENTATION DETAILS of a code change.
4253
4384
 
@@ -4340,7 +4471,8 @@ Each task object must have these exact fields:
4340
4471
  "description": "...", // 1-2 sentences, specific and actionable
4341
4472
  "layer": "data|service|api|view|route|test|infra", // implementation layer
4342
4473
  "filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
4343
- "acceptanceCriteria": ["..."], // verifiable completion conditions
4474
+ "acceptanceCriteria": ["..."], // behavioral completion conditions (the "what")
4475
+ "verificationSteps": ["..."], // concrete runnable checks with expected output (the "how to verify") \u2014 see rules below
4344
4476
  "dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
4345
4477
  "priority": "high|medium|low"
4346
4478
  }
@@ -4382,8 +4514,32 @@ CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
4382
4514
  - If you are unsure of the exact path for a new file, leave it as "TBD:<description>" rather than guessing.
4383
4515
  - 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.
4384
4516
 
4517
+ CRITICAL \u2014 verificationSteps Rules:
4518
+ Each step must be a concrete, self-contained check with an observable expected outcome.
4519
+
4520
+ Good examples (specific command + expected result):
4521
+ "POST /api/tasks with body {"title":"test"} \u2192 HTTP 201, response body contains {id, status:"pending"}"
4522
+ "GET /api/tasks/:id with unknown id \u2192 HTTP 404 with {code: 4040X, message: "..."}"
4523
+ "npm run build exits 0 with no TypeScript errors"
4524
+ "Prisma schema has Task model with fields: id, title, status, createdAt"
4525
+ "Store action createTask sets loading:true during request, loading:false on completion"
4526
+ "Route /tasks renders TaskList component, visible in router DevTools"
4527
+
4528
+ Bad examples (too vague \u2014 do NOT use these):
4529
+ "The endpoint works correctly" \u2717
4530
+ "Data is saved to the database" \u2717
4531
+ "UI displays the correct data" \u2717
4532
+ "Error handling works" \u2717
4533
+
4534
+ Rules:
4535
+ - At least 2 verification steps per task, max 5
4536
+ - Each step must be independently runnable/checkable
4537
+ - Backend tasks: include at least one HTTP request/response check and one data-layer check
4538
+ - Frontend tasks: include at least one UI render check and one state check
4539
+ - Build/compile tasks: always include "npm run build exits 0" or equivalent
4540
+
4385
4541
  Other rules:
4386
- - acceptanceCriteria must be verifiable (not vague like "works correctly")
4542
+ - acceptanceCriteria: behavioral statements ("order is created with pending status") \u2014 complementary to verificationSteps, not duplicates
4387
4543
  - dependencies must reflect real implementation order
4388
4544
  - Aim for 4-10 tasks total \u2014 not too granular, not too coarse
4389
4545
  - Each task should be completable in one focused coding session`;
@@ -4493,6 +4649,14 @@ function printTasks(tasks) {
4493
4649
  const badge = color(`[${task.layer}]`);
4494
4650
  const prio = task.priority === "high" ? chalk4.red("\u25CF") : task.priority === "medium" ? chalk4.yellow("\u25CF") : chalk4.gray("\u25CF");
4495
4651
  console.log(` ${prio} ${chalk4.bold(task.id)} ${badge} ${task.title}`);
4652
+ if (task.verificationSteps?.length) {
4653
+ for (const step of task.verificationSteps.slice(0, 2)) {
4654
+ console.log(chalk4.gray(` \u2713 ${step}`));
4655
+ }
4656
+ if (task.verificationSteps.length > 2) {
4657
+ console.log(chalk4.gray(` + ${task.verificationSteps.length - 2} more verification step(s)`));
4658
+ }
4659
+ }
4496
4660
  }
4497
4661
  }
4498
4662
  async function loadTasksForSpec(specFilePath) {
@@ -4571,6 +4735,21 @@ function validateDsl(raw) {
4571
4735
  for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
4572
4736
  validateEndpoint(eps[i], `endpoints[${i}]`, errors);
4573
4737
  }
4738
+ const seenEpIds = /* @__PURE__ */ new Set();
4739
+ for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
4740
+ const ep = eps[i];
4741
+ if (ep && typeof ep === "object" && typeof ep["id"] === "string") {
4742
+ const id = ep["id"];
4743
+ if (seenEpIds.has(id)) {
4744
+ errors.push({
4745
+ path: `endpoints[${i}].id`,
4746
+ message: `Duplicate endpoint id "${id}" \u2014 each endpoint must have a unique id`
4747
+ });
4748
+ } else {
4749
+ seenEpIds.add(id);
4750
+ }
4751
+ }
4752
+ }
4574
4753
  }
4575
4754
  if (obj["behaviors"] !== void 0) {
4576
4755
  if (!Array.isArray(obj["behaviors"])) {
@@ -4627,6 +4806,21 @@ function validateModel(raw, path10, errors) {
4627
4806
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4628
4807
  validateModelField(fields[j2], `${path10}.fields[${j2}]`, errors);
4629
4808
  }
4809
+ const seenFieldNames = /* @__PURE__ */ new Set();
4810
+ for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4811
+ const f = fields[j2];
4812
+ if (f && typeof f === "object" && typeof f["name"] === "string") {
4813
+ const name = f["name"];
4814
+ if (seenFieldNames.has(name)) {
4815
+ errors.push({
4816
+ path: `${path10}.fields[${j2}].name`,
4817
+ message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
4818
+ });
4819
+ } else {
4820
+ seenFieldNames.add(name);
4821
+ }
4822
+ }
4823
+ }
4630
4824
  }
4631
4825
  if (m["relations"] !== void 0) {
4632
4826
  if (!Array.isArray(m["relations"])) {
@@ -5657,9 +5851,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5657
5851
  console.log(chalk8.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5658
5852
  console.log(chalk8.gray(` Spec: ${specFilePath}`));
5659
5853
  try {
5660
- execSync(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
5854
+ spawnSync(claudeCmd, ["-p", promptContent], {
5661
5855
  cwd: workingDir,
5662
- stdio: "inherit"
5856
+ stdio: "inherit",
5857
+ shell: false
5663
5858
  });
5664
5859
  console.log(chalk8.green("\n \u2714 Claude Code completed."));
5665
5860
  } catch {
@@ -5711,9 +5906,10 @@ Full spec is at: ${specFilePath}
5711
5906
  Implement ONLY this task. Do not implement other tasks.`;
5712
5907
  let taskStatus = "done";
5713
5908
  try {
5714
- execSync(`${claudeCmd} -p "${taskPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`, {
5909
+ spawnSync(claudeCmd, ["-p", taskPrompt], {
5715
5910
  cwd: workingDir,
5716
- stdio: "inherit"
5911
+ stdio: "inherit",
5912
+ shell: false
5717
5913
  });
5718
5914
  completed++;
5719
5915
  } catch {
@@ -6116,27 +6312,202 @@ function printTaskProgress(completed, total, task, mode) {
6116
6312
  }
6117
6313
 
6118
6314
  // core/reviewer.ts
6119
- import chalk9 from "chalk";
6315
+ import chalk10 from "chalk";
6120
6316
  import { execSync as execSync2 } from "child_process";
6121
- import * as path7 from "path";
6317
+ import * as path8 from "path";
6318
+ import * as fs12 from "fs-extra";
6319
+
6320
+ // core/constitution-generator.ts
6321
+ import chalk9 from "chalk";
6122
6322
  import * as fs11 from "fs-extra";
6323
+ import * as path7 from "path";
6324
+
6325
+ // prompts/constitution.prompt.ts
6326
+ 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.
6327
+
6328
+ Output a Markdown document with EXACTLY these sections. Be specific and derive rules directly from the observed codebase \u2014 no generic advice.
6329
+
6330
+ ---
6331
+
6332
+ # Project Constitution
6333
+
6334
+ ## 1. \u67B6\u6784\u89C4\u5219 (Architecture Rules)
6335
+ \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
6336
+ - \u5206\u5C42\u67B6\u6784\u89C4\u5219\uFF08\u5982\uFF1Aroutes \u2192 controllers \u2192 services \u2192 DB\uFF09
6337
+ - \u7981\u6B62\u8DE8\u5C42\u76F4\u63A5\u8C03\u7528\u7684\u89C4\u5219
6338
+ - \u6A21\u5757\u7EC4\u7EC7\u89C4\u8303
6339
+
6340
+ ## 2. \u547D\u540D\u89C4\u8303 (Naming Conventions)
6341
+ - \u6587\u4EF6\u547D\u540D\u89C4\u5219\uFF08\u9A7C\u5CF0/\u4E0B\u5212\u7EBF/kebab\uFF09
6342
+ - \u53D8\u91CF\u3001\u51FD\u6570\u3001\u7C7B\u7684\u547D\u540D\u6A21\u5F0F
6343
+ - \u8DEF\u7531\u8DEF\u5F84\u7684\u547D\u540D\u89C4\u8303
6344
+
6345
+ ## 3. API \u89C4\u8303 (API Patterns)
6346
+ - \u8DEF\u7531\u524D\u7F00\u89C4\u5219\uFF08\u5982 /api/v1/client/... vs /api/v1/admin/...\uFF09
6347
+ - \u7EDF\u4E00\u54CD\u5E94\u7ED3\u6784\u6A21\u677F\uFF08code/message/data \u683C\u5F0F\uFF09
6348
+ - \u9519\u8BEF\u7801\u89C4\u8303\uFF08\u5DF2\u6709\u7684\u9519\u8BEF\u7801\u8303\u56F4\u548C\u542B\u4E49\uFF09
6349
+ - \u8BA4\u8BC1/\u9274\u6743\u6A21\u5F0F\uFF08middleware \u540D\u79F0\u548C\u4F7F\u7528\u4F4D\u7F6E\uFF09
6350
+
6351
+ ## 4. \u6570\u636E\u5C42\u89C4\u8303 (Data Layer Rules)
6352
+ - ORM/\u6570\u636E\u5E93\u8BBF\u95EE\u89C4\u5219\uFF08\u4EC5\u901A\u8FC7 service \u5C42\u8BBF\u95EE\uFF1F\u76F4\u63A5\u7528 Prisma/Mongoose\uFF1F\uFF09
6353
+ - \u5DF2\u6709\u7684\u6570\u636E\u6A21\u578B\u547D\u540D\u89C4\u8303
6354
+ - \u4E8B\u52A1\u5904\u7406\u6A21\u5F0F
6355
+
6356
+ ## 5. \u9519\u8BEF\u5904\u7406\u89C4\u8303 (Error Handling Patterns)
6357
+ - \u7EDF\u4E00\u9519\u8BEF\u5904\u7406 middleware \u7684\u4F7F\u7528\u89C4\u5219
6358
+ - \u9519\u8BEF\u629B\u51FA\u548C\u6355\u83B7\u7684\u6A21\u5F0F
6359
+ - \u5DF2\u77E5\u9519\u8BEF\u7801\u5217\u8868\uFF08\u4ECE\u4EE3\u7801\u4E2D\u63D0\u53D6\uFF09
6360
+
6361
+ ## 6. \u7981\u533A (Red Lines \u2014 Never Violate)
6362
+ \u660E\u786E\u5217\u51FA\u7EDD\u5BF9\u4E0D\u80FD\u505A\u7684\u4E8B\u60C5\uFF08\u4ECE\u73B0\u6709\u4EE3\u7801/\u67B6\u6784\u63A8\u65AD\uFF09\uFF1A
6363
+ - [ ] \u7981\u6B62 ...
6364
+ - [ ] \u7981\u6B62 ...
6365
+
6366
+ ## 7. \u6D4B\u8BD5\u89C4\u8303 (Testing Rules)
6367
+ - \u6D4B\u8BD5\u6587\u4EF6\u5B58\u653E\u4F4D\u7F6E
6368
+ - \u5FC5\u987B\u8986\u76D6\u7684\u6D4B\u8BD5\u573A\u666F\u7C7B\u578B
6369
+ - \u6D4B\u8BD5\u6846\u67B6\u548C\u5DE5\u5177
6370
+
6371
+ ## 8. \u5171\u4EAB\u914D\u7F6E\u6587\u4EF6\u6E05\u5355 (Shared Config Files \u2014 Append-Only)
6372
+
6373
+ CRITICAL: The following files are **singleton config files** that already exist in the project.
6374
+ When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
6375
+ appended/merged into these existing files. **NEVER create a new parallel file.**
6376
+
6377
+ For each discovered file, list it as:
6378
+ - \`<relative-path>\` \u2014 <category> \u2014 **MODIFY ONLY, never recreate**
6379
+
6380
+ If the project context includes i18n/locale files: list ALL of them with their paths.
6381
+ If the project context includes constants/enums files: list ALL of them.
6382
+ If the project context includes route index files: list ALL of them.
6383
+ If none are provided in the context, write: "(No shared config files detected \u2014 will be populated on first run)"
6384
+
6385
+ ---
6386
+
6387
+ Be concise. Each rule must be specific enough to enforce, not a vague principle.
6388
+ **Section 8 is the most important section for preventing file duplication bugs.**`;
6389
+
6390
+ // core/constitution-generator.ts
6391
+ var CONSTITUTION_FILE = ".ai-spec-constitution.md";
6392
+ var ConstitutionGenerator = class {
6393
+ constructor(provider) {
6394
+ this.provider = provider;
6395
+ }
6396
+ async generate(projectRoot) {
6397
+ const loader = new ContextLoader(projectRoot);
6398
+ const context = await loader.loadProjectContext();
6399
+ const prompt = buildConstitutionPrompt(context, projectRoot);
6400
+ return this.provider.generate(prompt, constitutionSystemPrompt);
6401
+ }
6402
+ async saveConstitution(projectRoot, content) {
6403
+ const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6404
+ await fs11.writeFile(filePath, content, "utf-8");
6405
+ return filePath;
6406
+ }
6407
+ };
6408
+ function buildConstitutionPrompt(context, projectRoot) {
6409
+ const parts = [
6410
+ "Analyze this project and generate its Project Constitution.\n",
6411
+ `=== Tech Stack ===
6412
+ ${context.techStack.join(", ") || "unknown"}
6413
+ `,
6414
+ `=== Dependencies (top 30) ===
6415
+ ${context.dependencies.slice(0, 30).join(", ")}
6416
+ `
6417
+ ];
6418
+ if (context.apiStructure.length > 0) {
6419
+ parts.push(`=== API/Route Files ===
6420
+ ${context.apiStructure.join("\n")}
6421
+ `);
6422
+ }
6423
+ if (context.routeSummary) {
6424
+ parts.push(`=== Route Code Samples ===
6425
+ ${context.routeSummary}
6426
+ `);
6427
+ }
6428
+ if (context.schema) {
6429
+ parts.push(`=== Prisma Schema ===
6430
+ ${context.schema.slice(0, 4e3)}
6431
+ `);
6432
+ }
6433
+ if (context.errorPatterns) {
6434
+ parts.push(`=== Error Handling Patterns ===
6435
+ ${context.errorPatterns}
6436
+ `);
6437
+ }
6438
+ if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
6439
+ const grouped = context.sharedConfigFiles.reduce(
6440
+ (acc, f) => {
6441
+ (acc[f.category] ??= []).push(f);
6442
+ return acc;
6443
+ },
6444
+ {}
6445
+ );
6446
+ const sections = [];
6447
+ for (const [category, files] of Object.entries(grouped)) {
6448
+ sections.push(`--- ${category} ---`);
6449
+ for (const f of files) {
6450
+ sections.push(`File: ${f.path}
6451
+ ${f.preview.slice(0, 600)}
6452
+ `);
6453
+ }
6454
+ }
6455
+ parts.push(`=== Existing Shared Config Files (Append-Only \u2014 NEVER Recreate) ===
6456
+ ${sections.join("\n")}
6457
+ `);
6458
+ }
6459
+ return parts.join("\n");
6460
+ }
6461
+ async function loadConstitution(projectRoot) {
6462
+ const filePath = path7.join(projectRoot, CONSTITUTION_FILE);
6463
+ if (await fs11.pathExists(filePath)) {
6464
+ return fs11.readFile(filePath, "utf-8");
6465
+ }
6466
+ return void 0;
6467
+ }
6468
+ function printConstitutionHint(exists) {
6469
+ if (!exists) {
6470
+ console.log(
6471
+ chalk9.yellow(
6472
+ " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6473
+ )
6474
+ );
6475
+ }
6476
+ }
6477
+
6478
+ // core/reviewer.ts
6479
+ async function loadAccumulatedLessons(projectRoot) {
6480
+ const constitutionPath = path8.join(projectRoot, CONSTITUTION_FILE);
6481
+ let content;
6482
+ try {
6483
+ content = await fs12.readFile(constitutionPath, "utf-8");
6484
+ } catch {
6485
+ return null;
6486
+ }
6487
+ const marker = "## 9. \u79EF\u7D2F\u6559\u8BAD";
6488
+ const idx = content.indexOf(marker);
6489
+ if (idx === -1) return null;
6490
+ const section = content.slice(idx);
6491
+ const nextSection = section.slice(marker.length).match(/\n## \d/);
6492
+ return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
6493
+ }
6123
6494
  var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
6124
6495
  async function loadReviewHistory(projectRoot) {
6125
- const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
6496
+ const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6126
6497
  try {
6127
- if (await fs11.pathExists(historyPath)) {
6128
- return await fs11.readJson(historyPath);
6498
+ if (await fs12.pathExists(historyPath)) {
6499
+ return await fs12.readJson(historyPath);
6129
6500
  }
6130
6501
  } catch {
6131
6502
  }
6132
6503
  return [];
6133
6504
  }
6134
6505
  async function appendReviewHistory(projectRoot, entry) {
6135
- const historyPath = path7.join(projectRoot, REVIEW_HISTORY_FILE);
6506
+ const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6136
6507
  const existing = await loadReviewHistory(projectRoot);
6137
6508
  const updated = [...existing, entry].slice(-20);
6138
6509
  try {
6139
- await fs11.writeJson(historyPath, updated, { spaces: 2 });
6510
+ await fs12.writeJson(historyPath, updated, { spaces: 2 });
6140
6511
  } catch {
6141
6512
  }
6142
6513
  }
@@ -6144,6 +6515,14 @@ function extractScore(reviewText) {
6144
6515
  const match = reviewText.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
6145
6516
  return match ? parseFloat(match[1]) : 0;
6146
6517
  }
6518
+ function extractComplianceScore(complianceText) {
6519
+ const match = complianceText.match(/ComplianceScore:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
6520
+ return match ? parseFloat(match[1]) : 0;
6521
+ }
6522
+ function extractMissingCount(complianceText) {
6523
+ const summaryMatch = complianceText.match(/Missing:\s*(\d+)/i);
6524
+ return summaryMatch ? parseInt(summaryMatch[1], 10) : 0;
6525
+ }
6147
6526
  function extractImpactLevel(reviewText) {
6148
6527
  const match = reviewText.match(/影响等级[::]\s*(低|中|高)/);
6149
6528
  return match ? match[1] : void 0;
@@ -6162,7 +6541,7 @@ function buildHistoryContext(history) {
6162
6541
  const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
6163
6542
  for (const entry of recent) {
6164
6543
  lines.push(`
6165
- [${entry.date}] ${path7.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6544
+ [${entry.date}] ${path8.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
6166
6545
  entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
6167
6546
  }
6168
6547
  return lines.join("\n") + "\n";
@@ -6197,23 +6576,52 @@ var CodeReviewer = class {
6197
6576
  };
6198
6577
  }
6199
6578
  /**
6200
- * Three-pass review:
6201
- * Pass 1architecture (spec compliance, layer separation, auth)
6579
+ * Four-pass review:
6580
+ * Pass 0 — spec compliance (exhaustive requirement coverage audit)
6581
+ * Pass 1 — architecture (layer separation, contract design, auth posture)
6202
6582
  * Pass 2 — implementation details (validation, error handling, edge cases)
6203
6583
  * + historical issue recurrence check
6204
6584
  * Pass 3 — impact assessment + code complexity
6205
6585
  */
6206
6586
  async runThreePassReview(specContent, codeContext, specFile) {
6207
- console.log(chalk9.gray(" Pass 1/3: Architecture review..."));
6208
- const archPrompt = `Review the architecture of this change.
6587
+ let complianceReview = "";
6588
+ if (specContent && specContent.trim() && specContent !== "(No spec \u2014 review for general code quality)") {
6589
+ console.log(chalk10.gray(" Pass 0/3: Spec compliance check..."));
6590
+ const compliancePrompt = `Check whether the implementation covers every requirement in the spec.
6209
6591
 
6592
+ === Feature Spec ===
6593
+ ${specContent}
6594
+
6595
+ === Code ===
6596
+ ${codeContext}`;
6597
+ complianceReview = await this.provider.generate(compliancePrompt, specComplianceSystemPrompt);
6598
+ const complianceScore2 = extractComplianceScore(complianceReview);
6599
+ const missingCount = extractMissingCount(complianceReview);
6600
+ if (complianceScore2 > 0) {
6601
+ const scoreColor = complianceScore2 >= 8 ? chalk10.green : complianceScore2 >= 6 ? chalk10.yellow : chalk10.red;
6602
+ console.log(
6603
+ chalk10.gray(" Pass 0 result: ") + scoreColor(`ComplianceScore ${complianceScore2}/10`) + (missingCount > 0 ? chalk10.red(` \xB7 ${missingCount} missing requirement(s)`) : chalk10.green(" \xB7 all requirements covered"))
6604
+ );
6605
+ }
6606
+ }
6607
+ console.log(chalk10.gray(` Pass 1/3: Architecture review...`));
6608
+ const accumulatedLessons = await loadAccumulatedLessons(this.projectRoot);
6609
+ const archPrompt = `Review the architecture of this change.
6610
+ ${complianceReview ? `
6611
+ === Spec Compliance Report (Pass 0 \u2014 already audited, do NOT re-audit missing requirements) ===
6612
+ ${complianceReview}
6613
+ ` : ""}
6614
+ ${accumulatedLessons ? `
6615
+ === \xA79 \u5386\u53F2\u79EF\u7D2F\u6559\u8BAD (Accumulated Lessons \u2014 check if any are repeated in this code) ===
6616
+ ${accumulatedLessons}
6617
+ ` : ""}
6210
6618
  === Feature Spec ===
6211
6619
  ${specContent || "(No spec \u2014 review for general code quality)"}
6212
6620
 
6213
6621
  === Code ===
6214
6622
  ${codeContext}`;
6215
6623
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6216
- console.log(chalk9.gray(" Pass 2/3: Implementation review..."));
6624
+ console.log(chalk10.gray(" Pass 2/3: Implementation review..."));
6217
6625
  const history = await loadReviewHistory(this.projectRoot);
6218
6626
  const historyContext = buildHistoryContext(history);
6219
6627
  const implPrompt = `Review the implementation details of this change.
@@ -6228,7 +6636,7 @@ ${codeContext}
6228
6636
  ${archReview}
6229
6637
  ${historyContext}`;
6230
6638
  const implReview = await this.provider.generate(implPrompt, reviewImplementationSystemPrompt);
6231
- console.log(chalk9.gray(" Pass 3/3: Impact & complexity assessment..."));
6639
+ console.log(chalk10.gray(" Pass 3/3: Impact & complexity assessment..."));
6232
6640
  const impactPrompt = `Assess the impact and complexity of this change.
6233
6641
 
6234
6642
  === Feature Spec ===
@@ -6244,24 +6652,23 @@ ${archReview}
6244
6652
  ${implReview}`;
6245
6653
  const impactReview = await this.provider.generate(impactPrompt, reviewImpactComplexitySystemPrompt);
6246
6654
  const sep = "\u2500".repeat(52);
6247
- const combined = `${archReview}
6655
+ const parts = complianceReview ? [complianceReview, archReview, implReview, impactReview] : [archReview, implReview, impactReview];
6656
+ const combined = parts.join(`
6248
6657
 
6249
6658
  ${sep}
6250
6659
 
6251
- ${implReview}
6252
-
6253
- ${sep}
6254
-
6255
- ${impactReview}`;
6660
+ `);
6256
6661
  const score = extractScore(implReview) || extractScore(archReview);
6662
+ const complianceScore = extractComplianceScore(complianceReview);
6257
6663
  const topIssues = extractTopIssues(implReview);
6258
6664
  const impactLevel = extractImpactLevel(impactReview);
6259
6665
  const complexityLevel = extractComplexityLevel(impactReview);
6260
6666
  if (score > 0 && specFile) {
6261
6667
  await appendReviewHistory(this.projectRoot, {
6262
6668
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
6263
- specFile: path7.relative(this.projectRoot, specFile),
6669
+ specFile: path8.relative(this.projectRoot, specFile),
6264
6670
  score,
6671
+ ...complianceScore > 0 ? { complianceScore } : {},
6265
6672
  topIssues,
6266
6673
  ...impactLevel ? { impactLevel } : {},
6267
6674
  ...complexityLevel ? { complexityLevel } : {}
@@ -6270,43 +6677,43 @@ ${impactReview}`;
6270
6677
  return combined;
6271
6678
  }
6272
6679
  async reviewCode(specContent, specFile) {
6273
- console.log(chalk9.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"));
6680
+ console.log(chalk10.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"));
6274
6681
  const diff = this.getGitDiff();
6275
6682
  if (!diff.trim()) {
6276
6683
  console.log(
6277
- chalk9.yellow(" No git diff found. Stage or commit changes first, then run review.")
6684
+ chalk10.yellow(" No git diff found. Stage or commit changes first, then run review.")
6278
6685
  );
6279
- console.log(chalk9.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6686
+ console.log(chalk10.gray(" Tip: run `git add .` then `ai-spec review` to review your work."));
6280
6687
  return "No changes";
6281
6688
  }
6282
6689
  const { files, added, removed } = this.getDiffStats(diff);
6283
6690
  console.log(
6284
- chalk9.gray(` Diff: ${files} file(s), ${chalk9.green("+" + added)} ${chalk9.red("-" + removed)}`)
6691
+ chalk10.gray(` Diff: ${files} file(s), ${chalk10.green("+" + added)} ${chalk10.red("-" + removed)}`)
6285
6692
  );
6286
6693
  console.log(
6287
- chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6694
+ chalk10.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6288
6695
  );
6289
6696
  const codeContext = diff.slice(0, 1e4);
6290
6697
  const reviewResult = await this.runThreePassReview(specContent, codeContext, specFile);
6291
- console.log(chalk9.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"));
6698
+ console.log(chalk10.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"));
6292
6699
  console.log(reviewResult);
6293
- console.log(chalk9.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"));
6700
+ console.log(chalk10.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"));
6294
6701
  return reviewResult;
6295
6702
  }
6296
6703
  /**
6297
6704
  * Review directly from generated file contents (for api mode where git diff is empty).
6298
6705
  */
6299
6706
  async reviewFiles(specContent, filePaths, workingDir, specFile) {
6300
- console.log(chalk9.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6301
- console.log(chalk9.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6707
+ console.log(chalk10.cyan("\n\u2500\u2500\u2500 Automated Code Review (file-based) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6708
+ console.log(chalk10.gray(` Reviewing ${filePaths.length} generated file(s)...`));
6302
6709
  console.log(
6303
- chalk9.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6710
+ chalk10.blue(` Reviewing with ${this.provider.providerName}/${this.provider.modelName}...`)
6304
6711
  );
6305
6712
  let filesSection = "";
6306
6713
  for (const filePath of filePaths) {
6307
- const fullPath = path7.join(workingDir, filePath);
6714
+ const fullPath = path8.join(workingDir, filePath);
6308
6715
  try {
6309
- const content = await fs11.readFile(fullPath, "utf-8");
6716
+ const content = await fs12.readFile(fullPath, "utf-8");
6310
6717
  filesSection += `
6311
6718
 
6312
6719
  === ${filePath} ===
@@ -6321,189 +6728,31 @@ ${content.slice(0, 3e3)}`;
6321
6728
  }
6322
6729
  }
6323
6730
  const reviewResult = await this.runThreePassReview(specContent, filesSection, specFile);
6324
- console.log(chalk9.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"));
6731
+ console.log(chalk10.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"));
6325
6732
  console.log(reviewResult);
6326
- console.log(chalk9.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"));
6733
+ console.log(chalk10.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"));
6327
6734
  return reviewResult;
6328
6735
  }
6329
6736
  /** Print score trend from history (last N reviews) */
6330
6737
  async printScoreTrend(limit = 5) {
6331
6738
  const history = await loadReviewHistory(this.projectRoot);
6332
6739
  if (history.length === 0) {
6333
- console.log(chalk9.gray(" No review history yet."));
6740
+ console.log(chalk10.gray(" No review history yet."));
6334
6741
  return;
6335
6742
  }
6336
6743
  const recent = history.slice(-limit);
6337
- console.log(chalk9.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"));
6744
+ console.log(chalk10.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"));
6338
6745
  for (const entry of recent) {
6339
6746
  const bar = "\u2588".repeat(entry.score) + "\u2591".repeat(10 - entry.score);
6340
- const color = entry.score >= 8 ? chalk9.green : entry.score >= 6 ? chalk9.yellow : chalk9.red;
6341
- const impactTag = entry.impactLevel ? chalk9.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk9.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk9.yellow(entry.impactLevel) : chalk9.green(entry.impactLevel)}`) : "";
6342
- const complexityTag = entry.complexityLevel ? chalk9.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk9.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk9.yellow(entry.complexityLevel) : chalk9.green(entry.complexityLevel)}`) : "";
6343
- console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path7.basename(entry.specFile)}`);
6747
+ const color = entry.score >= 8 ? chalk10.green : entry.score >= 6 ? chalk10.yellow : chalk10.red;
6748
+ const impactTag = entry.impactLevel ? chalk10.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk10.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk10.yellow(entry.impactLevel) : chalk10.green(entry.impactLevel)}`) : "";
6749
+ const complexityTag = entry.complexityLevel ? chalk10.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk10.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk10.yellow(entry.complexityLevel) : chalk10.green(entry.complexityLevel)}`) : "";
6750
+ console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path8.basename(entry.specFile)}`);
6344
6751
  }
6345
- console.log(chalk9.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"));
6752
+ console.log(chalk10.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"));
6346
6753
  }
6347
6754
  };
6348
6755
 
6349
- // core/constitution-generator.ts
6350
- import chalk10 from "chalk";
6351
- import * as fs12 from "fs-extra";
6352
- import * as path8 from "path";
6353
-
6354
- // prompts/constitution.prompt.ts
6355
- 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.
6356
-
6357
- Output a Markdown document with EXACTLY these sections. Be specific and derive rules directly from the observed codebase \u2014 no generic advice.
6358
-
6359
- ---
6360
-
6361
- # Project Constitution
6362
-
6363
- ## 1. \u67B6\u6784\u89C4\u5219 (Architecture Rules)
6364
- \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
6365
- - \u5206\u5C42\u67B6\u6784\u89C4\u5219\uFF08\u5982\uFF1Aroutes \u2192 controllers \u2192 services \u2192 DB\uFF09
6366
- - \u7981\u6B62\u8DE8\u5C42\u76F4\u63A5\u8C03\u7528\u7684\u89C4\u5219
6367
- - \u6A21\u5757\u7EC4\u7EC7\u89C4\u8303
6368
-
6369
- ## 2. \u547D\u540D\u89C4\u8303 (Naming Conventions)
6370
- - \u6587\u4EF6\u547D\u540D\u89C4\u5219\uFF08\u9A7C\u5CF0/\u4E0B\u5212\u7EBF/kebab\uFF09
6371
- - \u53D8\u91CF\u3001\u51FD\u6570\u3001\u7C7B\u7684\u547D\u540D\u6A21\u5F0F
6372
- - \u8DEF\u7531\u8DEF\u5F84\u7684\u547D\u540D\u89C4\u8303
6373
-
6374
- ## 3. API \u89C4\u8303 (API Patterns)
6375
- - \u8DEF\u7531\u524D\u7F00\u89C4\u5219\uFF08\u5982 /api/v1/client/... vs /api/v1/admin/...\uFF09
6376
- - \u7EDF\u4E00\u54CD\u5E94\u7ED3\u6784\u6A21\u677F\uFF08code/message/data \u683C\u5F0F\uFF09
6377
- - \u9519\u8BEF\u7801\u89C4\u8303\uFF08\u5DF2\u6709\u7684\u9519\u8BEF\u7801\u8303\u56F4\u548C\u542B\u4E49\uFF09
6378
- - \u8BA4\u8BC1/\u9274\u6743\u6A21\u5F0F\uFF08middleware \u540D\u79F0\u548C\u4F7F\u7528\u4F4D\u7F6E\uFF09
6379
-
6380
- ## 4. \u6570\u636E\u5C42\u89C4\u8303 (Data Layer Rules)
6381
- - ORM/\u6570\u636E\u5E93\u8BBF\u95EE\u89C4\u5219\uFF08\u4EC5\u901A\u8FC7 service \u5C42\u8BBF\u95EE\uFF1F\u76F4\u63A5\u7528 Prisma/Mongoose\uFF1F\uFF09
6382
- - \u5DF2\u6709\u7684\u6570\u636E\u6A21\u578B\u547D\u540D\u89C4\u8303
6383
- - \u4E8B\u52A1\u5904\u7406\u6A21\u5F0F
6384
-
6385
- ## 5. \u9519\u8BEF\u5904\u7406\u89C4\u8303 (Error Handling Patterns)
6386
- - \u7EDF\u4E00\u9519\u8BEF\u5904\u7406 middleware \u7684\u4F7F\u7528\u89C4\u5219
6387
- - \u9519\u8BEF\u629B\u51FA\u548C\u6355\u83B7\u7684\u6A21\u5F0F
6388
- - \u5DF2\u77E5\u9519\u8BEF\u7801\u5217\u8868\uFF08\u4ECE\u4EE3\u7801\u4E2D\u63D0\u53D6\uFF09
6389
-
6390
- ## 6. \u7981\u533A (Red Lines \u2014 Never Violate)
6391
- \u660E\u786E\u5217\u51FA\u7EDD\u5BF9\u4E0D\u80FD\u505A\u7684\u4E8B\u60C5\uFF08\u4ECE\u73B0\u6709\u4EE3\u7801/\u67B6\u6784\u63A8\u65AD\uFF09\uFF1A
6392
- - [ ] \u7981\u6B62 ...
6393
- - [ ] \u7981\u6B62 ...
6394
-
6395
- ## 7. \u6D4B\u8BD5\u89C4\u8303 (Testing Rules)
6396
- - \u6D4B\u8BD5\u6587\u4EF6\u5B58\u653E\u4F4D\u7F6E
6397
- - \u5FC5\u987B\u8986\u76D6\u7684\u6D4B\u8BD5\u573A\u666F\u7C7B\u578B
6398
- - \u6D4B\u8BD5\u6846\u67B6\u548C\u5DE5\u5177
6399
-
6400
- ## 8. \u5171\u4EAB\u914D\u7F6E\u6587\u4EF6\u6E05\u5355 (Shared Config Files \u2014 Append-Only)
6401
-
6402
- CRITICAL: The following files are **singleton config files** that already exist in the project.
6403
- When any feature needs to add entries (translations, constants, routes, enums, etc.), they MUST be
6404
- appended/merged into these existing files. **NEVER create a new parallel file.**
6405
-
6406
- For each discovered file, list it as:
6407
- - \`<relative-path>\` \u2014 <category> \u2014 **MODIFY ONLY, never recreate**
6408
-
6409
- If the project context includes i18n/locale files: list ALL of them with their paths.
6410
- If the project context includes constants/enums files: list ALL of them.
6411
- If the project context includes route index files: list ALL of them.
6412
- If none are provided in the context, write: "(No shared config files detected \u2014 will be populated on first run)"
6413
-
6414
- ---
6415
-
6416
- Be concise. Each rule must be specific enough to enforce, not a vague principle.
6417
- **Section 8 is the most important section for preventing file duplication bugs.**`;
6418
-
6419
- // core/constitution-generator.ts
6420
- var CONSTITUTION_FILE = ".ai-spec-constitution.md";
6421
- var ConstitutionGenerator = class {
6422
- constructor(provider) {
6423
- this.provider = provider;
6424
- }
6425
- async generate(projectRoot) {
6426
- const loader = new ContextLoader(projectRoot);
6427
- const context = await loader.loadProjectContext();
6428
- const prompt = buildConstitutionPrompt(context, projectRoot);
6429
- return this.provider.generate(prompt, constitutionSystemPrompt);
6430
- }
6431
- async saveConstitution(projectRoot, content) {
6432
- const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6433
- await fs12.writeFile(filePath, content, "utf-8");
6434
- return filePath;
6435
- }
6436
- };
6437
- function buildConstitutionPrompt(context, projectRoot) {
6438
- const parts = [
6439
- "Analyze this project and generate its Project Constitution.\n",
6440
- `=== Tech Stack ===
6441
- ${context.techStack.join(", ") || "unknown"}
6442
- `,
6443
- `=== Dependencies (top 30) ===
6444
- ${context.dependencies.slice(0, 30).join(", ")}
6445
- `
6446
- ];
6447
- if (context.apiStructure.length > 0) {
6448
- parts.push(`=== API/Route Files ===
6449
- ${context.apiStructure.join("\n")}
6450
- `);
6451
- }
6452
- if (context.routeSummary) {
6453
- parts.push(`=== Route Code Samples ===
6454
- ${context.routeSummary}
6455
- `);
6456
- }
6457
- if (context.schema) {
6458
- parts.push(`=== Prisma Schema ===
6459
- ${context.schema.slice(0, 4e3)}
6460
- `);
6461
- }
6462
- if (context.errorPatterns) {
6463
- parts.push(`=== Error Handling Patterns ===
6464
- ${context.errorPatterns}
6465
- `);
6466
- }
6467
- if (context.sharedConfigFiles && context.sharedConfigFiles.length > 0) {
6468
- const grouped = context.sharedConfigFiles.reduce(
6469
- (acc, f) => {
6470
- (acc[f.category] ??= []).push(f);
6471
- return acc;
6472
- },
6473
- {}
6474
- );
6475
- const sections = [];
6476
- for (const [category, files] of Object.entries(grouped)) {
6477
- sections.push(`--- ${category} ---`);
6478
- for (const f of files) {
6479
- sections.push(`File: ${f.path}
6480
- ${f.preview.slice(0, 600)}
6481
- `);
6482
- }
6483
- }
6484
- parts.push(`=== Existing Shared Config Files (Append-Only \u2014 NEVER Recreate) ===
6485
- ${sections.join("\n")}
6486
- `);
6487
- }
6488
- return parts.join("\n");
6489
- }
6490
- async function loadConstitution(projectRoot) {
6491
- const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
6492
- if (await fs12.pathExists(filePath)) {
6493
- return fs12.readFile(filePath, "utf-8");
6494
- }
6495
- return void 0;
6496
- }
6497
- function printConstitutionHint(exists) {
6498
- if (!exists) {
6499
- console.log(
6500
- chalk10.yellow(
6501
- " \u26A1 Tip: Run `ai-spec init` to generate a Project Constitution for better spec quality."
6502
- )
6503
- );
6504
- }
6505
- }
6506
-
6507
6756
  // core/combined-generator.ts
6508
6757
  var TASKS_SEPARATOR = "---TASKS_JSON---";
6509
6758
  var tasksInstruction = `
@@ -6512,11 +6761,21 @@ var tasksInstruction = `
6512
6761
  After outputting the complete spec above, append EXACTLY this line on its own (no extra text before or after it):
6513
6762
  ${TASKS_SEPARATOR}
6514
6763
  Then output a valid JSON array of implementation tasks. Each element must have these exact fields:
6515
- {"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"}
6764
+ {"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"}
6765
+ 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.
6516
6766
  Layer order: data \u2192 infra \u2192 service \u2192 api \u2192 test. 4-10 tasks total. filesToTouch must use real paths from the project context.`;
6517
- async function generateSpecWithTasks(provider, idea, context) {
6767
+ async function generateSpecWithTasks(provider, idea, context, architectureDecision) {
6518
6768
  const contextBlock = buildTaskPrompt("", context).trim();
6519
- const fullPrompt = [idea, contextBlock].filter(Boolean).join("\n\n");
6769
+ const parts = [idea];
6770
+ if (architectureDecision) {
6771
+ parts.push(
6772
+ `
6773
+ === Architecture Decision (MUST follow this approach in the spec) ===
6774
+ ${architectureDecision}`
6775
+ );
6776
+ }
6777
+ if (contextBlock) parts.push(contextBlock);
6778
+ const fullPrompt = parts.join("\n\n");
6520
6779
  const combinedSystemPrompt = specPrompt + tasksInstruction;
6521
6780
  const raw = await provider.generate(fullPrompt, combinedSystemPrompt);
6522
6781
  return parseSpecAndTasks(raw);
@@ -6651,6 +6910,9 @@ export {
6651
6910
  TaskGenerator,
6652
6911
  buildTaskPrompt,
6653
6912
  createProvider,
6913
+ extractBehavioralContract,
6914
+ extractComplianceScore,
6915
+ extractMissingCount,
6654
6916
  generateSpecWithTasks,
6655
6917
  isFrontendDeps,
6656
6918
  loadConstitution,