ai-spec-dev 0.14.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -147,7 +147,7 @@ declare class SpecRefiner {
147
147
  }
148
148
 
149
149
  declare function buildTaskPrompt(spec: string, context?: ProjectContext): string;
150
- type TaskLayer = "data" | "infra" | "service" | "api" | "test";
150
+ type TaskLayer = "data" | "infra" | "service" | "api" | "view" | "route" | "test";
151
151
  type TaskPriority = "high" | "medium" | "low";
152
152
  type TaskStatus = "pending" | "done" | "failed";
153
153
  interface SpecTask {
package/dist/index.d.ts CHANGED
@@ -147,7 +147,7 @@ declare class SpecRefiner {
147
147
  }
148
148
 
149
149
  declare function buildTaskPrompt(spec: string, context?: ProjectContext): string;
150
- type TaskLayer = "data" | "infra" | "service" | "api" | "test";
150
+ type TaskLayer = "data" | "infra" | "service" | "api" | "view" | "route" | "test";
151
151
  type TaskPriority = "high" | "medium" | "low";
152
152
  type TaskStatus = "pending" | "done" | "failed";
153
153
  interface SpecTask {
package/dist/index.js CHANGED
@@ -3684,7 +3684,7 @@ ${content.slice(0, 1500)}`);
3684
3684
  try {
3685
3685
  const content = await fs3.readFile(path2.join(this.projectRoot, f), "utf-8");
3686
3686
  parts.push(`// ${f}
3687
- ${content.slice(0, 800)}`);
3687
+ ${content.slice(0, 2e3)}`);
3688
3688
  } catch {
3689
3689
  }
3690
3690
  }
@@ -4093,9 +4093,17 @@ CRITICAL \u2014 Route/Store index registration (MUST follow):
4093
4093
 
4094
4094
  CRITICAL \u2014 Cross-file function name consistency (MUST follow):
4095
4095
  17. When you see an "=== Files Already Generated in This Run ===" section, those file contents are the AUTHORITATIVE source
4096
- of truth for exported function/variable names.
4097
- NEVER rename, guess, or substitute alternative names. If the file exports "getTaskList", import "getTaskList" \u2014 not "getTasks".
4098
- If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.`;
4096
+ of truth for exported function/variable/action names.
4097
+ NEVER rename, guess, or substitute alternative names. Copy-paste the exact identifier.
4098
+ Common hallucination patterns to AVOID:
4099
+ - Adding suffixes: fetchTasks \u2192 fetchTaskList, fetchTaskData, fetchTaskAll \u2190 ALL WRONG
4100
+ - Changing verb: fetchTasks \u2192 getTasks, loadTasks, queryTasks \u2190 WRONG unless that's in the cache
4101
+ - Changing number: createTask \u2192 createTasks \u2190 WRONG
4102
+ For Pinia stores specifically: the "// public API (return object):" section or the full store content
4103
+ shows EVERY available action name. If it shows "fetchTasks", that is the ONLY valid name.
4104
+ If no such section is present, derive function names strictly from the DSL endpoint IDs shown in the spec.
4105
+ ALSO applies to file paths: if you see "// exists: src/views/task-management/TaskManagement.vue",
4106
+ the router import MUST use that exact path \u2014 NOT "@/views/task-management/index.vue" or any other guess.`;
4099
4107
  var codeGenGoSystemPrompt = `You are a Senior Go Developer implementing features based on provided specifications.
4100
4108
 
4101
4109
  Rules:
@@ -4270,7 +4278,7 @@ Each task object must have these exact fields:
4270
4278
  "id": "TASK-001", // sequential, zero-padded
4271
4279
  "title": "...", // short action phrase, e.g. "Add UserFavorite Prisma model"
4272
4280
  "description": "...", // 1-2 sentences, specific and actionable
4273
- "layer": "data|service|api|test|infra", // implementation layer
4281
+ "layer": "data|service|api|view|route|test|infra", // implementation layer
4274
4282
  "filesToTouch": ["..."], // VERIFIED paths only \u2014 see rules below
4275
4283
  "acceptanceCriteria": ["..."], // verifiable completion conditions
4276
4284
  "dependencies": ["TASK-001"], // task ids that must complete first (empty array if none)
@@ -4278,11 +4286,34 @@ Each task object must have these exact fields:
4278
4286
  }
4279
4287
 
4280
4288
  Layer ordering guidance (implement in this order):
4281
- 1. "data" \u2014 DB schema changes, migrations, seed data
4289
+ 1. "data" \u2014 DB schema changes, migrations, seed data; TypeScript type/interface definition files
4282
4290
  2. "infra" \u2014 config, env vars, external service setup
4283
- 3. "service" \u2014 business logic, service classes
4284
- 4. "api" \u2014 controllers, routes, middleware, validators
4285
- 5. "test" \u2014 unit tests, integration tests
4291
+ 3. "service" \u2014 business logic, service classes; for frontend: HTTP API call files ONLY (src/api/ or src/apis/)
4292
+ 4. "api" \u2014 controllers, routes, middleware, validators; for frontend: state stores ONLY (src/stores/, Pinia/Vuex/Zustand/Redux)
4293
+ 5. "view" \u2014 FRONTEND ONLY: page/view components (src/views/, src/pages/) \u2014 generated AFTER stores
4294
+ 6. "route" \u2014 FRONTEND ONLY: router module files (src/router/routes/) \u2014 generated AFTER view components
4295
+ 7. "test" \u2014 unit tests, integration tests
4296
+
4297
+ CRITICAL \u2014 Frontend four-layer dependency rule (prevents BOTH naming AND filename hallucinations):
4298
+ For Vue/React frontend projects, STRICTLY follow this assignment:
4299
+ "service" \u2192 src/api/* or src/apis/* files (HTTP functions: getTaskList, createTask)
4300
+ "api" \u2192 src/stores/* files (stores call service layer \u2014 see exact function names)
4301
+ "view" \u2192 src/views/* or src/pages/* files (pages use stores \u2014 see exact action names)
4302
+ "route" \u2192 src/router/routes/* files (router imports views \u2014 sees EXACT component filenames)
4303
+
4304
+ WHY "route" must come after "view":
4305
+ The router file imports the view component by filename, e.g.:
4306
+ import('@/views/task-management/TaskManagement.vue')
4307
+ If the router is generated BEFORE the view file exists in cache, the AI will guess a generic name
4308
+ like "index.vue" (the most common fallback) instead of the real filename "TaskManagement.vue".
4309
+ By generating the router AFTER the view, the cache contains "// exists: src/views/task-management/TaskManagement.vue"
4310
+ and the AI uses the EXACT path.
4311
+
4312
+ EXAMPLE (correct, four-layer):
4313
+ TASK-001 layer:"service" src/apis/taskManagement.ts (exports getTaskList, createTask)
4314
+ TASK-002 layer:"api" src/stores/taskStore.ts (calls getTaskList \u2014 visible in cache \u2713)
4315
+ TASK-003 layer:"view" src/views/task-management/TaskManagement.vue (uses taskStore \u2014 visible in cache \u2713)
4316
+ TASK-004 layer:"route" src/router/routes/taskManagement.ts (imports TaskManagement.vue \u2014 filename visible in cache \u2713)
4286
4317
 
4287
4318
  CRITICAL \u2014 filesToTouch Rules (hallucination prevention):
4288
4319
  - ONLY use paths that appear in the "Verified File Inventory" section of the prompt.
@@ -4332,7 +4363,7 @@ function buildTaskPrompt(spec, context) {
4332
4363
  if (context.constitution) {
4333
4364
  parts.push(`
4334
4365
  === Project Constitution (rules to follow) ===
4335
- ${context.constitution.slice(0, 1500)}`);
4366
+ ${context.constitution}`);
4336
4367
  }
4337
4368
  if (context.techStack.length > 0) {
4338
4369
  parts.push(`
@@ -4347,7 +4378,9 @@ var LAYER_ORDER = {
4347
4378
  infra: 1,
4348
4379
  service: 2,
4349
4380
  api: 3,
4350
- test: 4
4381
+ view: 4,
4382
+ route: 5,
4383
+ test: 6
4351
4384
  };
4352
4385
  var TaskGenerator = class {
4353
4386
  constructor(provider) {
@@ -4389,6 +4422,8 @@ function printTasks(tasks) {
4389
4422
  infra: import_chalk3.default.gray,
4390
4423
  service: import_chalk3.default.blue,
4391
4424
  api: import_chalk3.default.cyan,
4425
+ view: import_chalk3.default.yellow,
4426
+ route: import_chalk3.default.white,
4392
4427
  test: import_chalk3.default.green
4393
4428
  };
4394
4429
  console.log(import_chalk3.default.bold(`
@@ -5270,16 +5305,111 @@ function buildInstalledPackagesSection(context) {
5270
5305
  ${context.dependencies.join(", ")}
5271
5306
  `;
5272
5307
  }
5308
+ function extractBehavioralContract(content) {
5309
+ const lines = content.split("\n");
5310
+ const contractLines = [];
5311
+ const throwLines = [];
5312
+ let i = 0;
5313
+ while (i < lines.length) {
5314
+ const line = lines[i];
5315
+ const trimmed = line.trim();
5316
+ if (/^export\s+(interface|type|class|abstract\s+class|enum)\s/.test(trimmed)) {
5317
+ contractLines.push(line.trimEnd());
5318
+ if (trimmed.includes("{")) {
5319
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5320
+ i++;
5321
+ while (i < lines.length && depth > 0) {
5322
+ const inner = lines[i];
5323
+ contractLines.push(inner.trimEnd());
5324
+ depth += (inner.match(/\{/g) ?? []).length;
5325
+ depth -= (inner.match(/\}/g) ?? []).length;
5326
+ i++;
5327
+ }
5328
+ } else {
5329
+ i++;
5330
+ }
5331
+ continue;
5332
+ }
5333
+ if (/^export\s+const\s+\w+\s*=\s*(defineStore|createStore|createSlice)\s*\(/.test(trimmed)) {
5334
+ contractLines.push(line.trimEnd());
5335
+ let depth = (trimmed.match(/\(/g) ?? []).length - (trimmed.match(/\)/g) ?? []).length;
5336
+ i++;
5337
+ while (i < lines.length && depth > 0) {
5338
+ const inner = lines[i];
5339
+ contractLines.push(inner.trimEnd());
5340
+ depth += (inner.match(/\(/g) ?? []).length;
5341
+ depth -= (inner.match(/\)/g) ?? []).length;
5342
+ i++;
5343
+ }
5344
+ continue;
5345
+ }
5346
+ if (/^return\s*\{/.test(trimmed)) {
5347
+ contractLines.push("// public API (return object):");
5348
+ contractLines.push(line.trimEnd());
5349
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5350
+ i++;
5351
+ while (i < lines.length && depth > 0) {
5352
+ const inner = lines[i];
5353
+ contractLines.push(inner.trimEnd());
5354
+ depth += (inner.match(/\{/g) ?? []).length;
5355
+ depth -= (inner.match(/\}/g) ?? []).length;
5356
+ i++;
5357
+ }
5358
+ continue;
5359
+ }
5360
+ if (/^export\s+default\s+(async\s+)?(function|class)\b/.test(trimmed)) {
5361
+ contractLines.push(line.trimEnd());
5362
+ if (trimmed.includes("{")) {
5363
+ let depth = (trimmed.match(/\{/g) ?? []).length - (trimmed.match(/\}/g) ?? []).length;
5364
+ i++;
5365
+ while (i < lines.length && depth > 0) {
5366
+ const inner = lines[i];
5367
+ contractLines.push(inner.trimEnd());
5368
+ depth += (inner.match(/\{/g) ?? []).length;
5369
+ depth -= (inner.match(/\}/g) ?? []).length;
5370
+ i++;
5371
+ }
5372
+ } else {
5373
+ i++;
5374
+ }
5375
+ continue;
5376
+ }
5377
+ if (/^export\s/.test(trimmed)) {
5378
+ contractLines.push(line.trimEnd());
5379
+ }
5380
+ if (/throw\s+(new\s+)?\w*[Ee]rror\b|throw\s+create[A-Z]\w*|@throws/.test(line) && throwLines.length < 20) {
5381
+ throwLines.push(" // " + trimmed);
5382
+ }
5383
+ i++;
5384
+ }
5385
+ if (contractLines.length === 0 && throwLines.length === 0) {
5386
+ return content.slice(0, 3e3);
5387
+ }
5388
+ const parts = [...contractLines];
5389
+ if (throwLines.length > 0) {
5390
+ parts.push("", "// Error contracts (throws / validation):", ...throwLines);
5391
+ }
5392
+ return parts.join("\n");
5393
+ }
5273
5394
  function buildGeneratedFilesSection(cache) {
5274
5395
  if (cache.size === 0) return "";
5275
5396
  const lines = [
5276
- "\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ==="
5397
+ "\n=== Files Already Generated in This Run \u2014 USE EXACT EXPORTS (do not rename or invent alternatives) ===",
5398
+ "// CRITICAL: function/action names and file paths below are ground truth. Copy them EXACTLY.",
5399
+ "// Do NOT add suffixes (List, Data, All, Info) or change casing.",
5400
+ "// For '// exists:' entries: use the EXACT filename shown \u2014 do NOT substitute index.vue or other defaults."
5277
5401
  ];
5278
5402
  for (const [filePath, content] of cache) {
5403
+ const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(filePath);
5404
+ if (isViewFile) {
5405
+ lines.push(`
5406
+ // exists: ${filePath}`);
5407
+ continue;
5408
+ }
5279
5409
  lines.push(`
5280
5410
  --- ${filePath} ---`);
5281
- lines.push(content.slice(0, 800));
5282
- if (content.length > 800) lines.push("... (truncated)");
5411
+ const isStoreOrComposable = /src[\\/](stores?|composables?)[\\/]/i.test(filePath);
5412
+ lines.push(isStoreOrComposable ? content : extractBehavioralContract(content));
5283
5413
  }
5284
5414
  return lines.join("\n") + "\n";
5285
5415
  }
@@ -5467,7 +5597,7 @@ Implement ONLY this task. Do not implement other tasks.`;
5467
5597
  const spec = await fs8.readFile(specFilePath, "utf-8");
5468
5598
  const constitutionSection = context?.constitution ? `
5469
5599
  === Project Constitution (MUST follow) ===
5470
- ${context.constitution.slice(0, 2e3)}
5600
+ ${context.constitution}
5471
5601
  ` : "";
5472
5602
  const contextSummary = context ? `Tech Stack: ${context.techStack.join(", ")}
5473
5603
  Existing files: ${context.fileStructure.slice(0, 20).join(", ")}` : "";
@@ -5561,7 +5691,7 @@ Output ONLY a valid JSON array:
5561
5691
  printTaskProgress(completedTasks++, tasks.length, task, "skip");
5562
5692
  }
5563
5693
  }
5564
- const LAYER_ORDER2 = ["data", "infra", "service", "api", "test"];
5694
+ const LAYER_ORDER2 = ["data", "infra", "service", "api", "view", "route", "test"];
5565
5695
  const layerGroups = [];
5566
5696
  for (const layer of LAYER_ORDER2) {
5567
5697
  const group = pendingTasks.filter((t) => t.layer === layer);
@@ -5584,10 +5714,9 @@ Output ONLY a valid JSON array:
5584
5714
  } else {
5585
5715
  printTaskProgress(completedTasks, tasks.length, layerTasks[0], "run");
5586
5716
  }
5587
- const generatedFilesSection = buildGeneratedFilesSection(generatedFileCache);
5588
- const taskResultPromises = layerTasks.map(async (task) => {
5717
+ const executeTask = async (task, batchIsParallel) => {
5589
5718
  if (task.filesToTouch.length === 0) {
5590
- if (!isParallel) console.log(import_chalk6.default.gray(" No files specified, skipping."));
5719
+ if (!batchIsParallel) console.log(import_chalk6.default.gray(" No files specified, skipping."));
5591
5720
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false };
5592
5721
  }
5593
5722
  const filePlan = await Promise.all(
@@ -5602,10 +5731,11 @@ Output ONLY a valid JSON array:
5602
5731
  );
5603
5732
  const createsNewFiles = filePlan.some((f) => f.action === "create");
5604
5733
  const taskText = `${task.title} ${task.description}`.toLowerCase();
5605
- const impliesRegistration = createsNewFiles && (taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
5734
+ const impliesRegistration = createsNewFiles && (task.layer === "route" || task.layer === "view" || task.layer === "api" || taskText.includes("route") || taskText.includes("router") || taskText.includes("page") || taskText.includes("view") || taskText.includes("store") || taskText.includes("service") || taskText.includes("component") || taskText.includes("menu") || taskText.includes("navigation") || taskText.includes("\u6A21\u5757") || taskText.includes("\u9875\u9762") || taskText.includes("\u8DEF\u7531") || taskText.includes("\u6CE8\u518C"));
5606
5735
  if (filePlan.length === 0) {
5607
5736
  return { task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration };
5608
5737
  }
5738
+ const currentGeneratedFilesSection = buildGeneratedFilesSection(generatedFileCache);
5609
5739
  const taskContext = `Task: ${task.id} \u2014 ${task.title}
5610
5740
  ${task.description}
5611
5741
  Acceptance: ${task.acceptanceCriteria.join("; ")}`;
@@ -5616,15 +5746,38 @@ Acceptance: ${task.acceptanceCriteria.join("; ")}`;
5616
5746
  === Current Task ===
5617
5747
  ${taskContext}`,
5618
5748
  workingDir,
5619
- constitutionSection + frontendSection + sharedConfigSection + generatedFilesSection,
5749
+ constitutionSection + frontendSection + sharedConfigSection + currentGeneratedFilesSection,
5620
5750
  systemPrompt,
5621
- isParallel ? task.id : void 0
5751
+ batchIsParallel ? task.id : void 0
5622
5752
  // prefix output lines with task ID in parallel mode
5623
5753
  );
5624
5754
  const createdFiles = filePlan.filter((fp) => fp.action === "create").map((fp) => fp.file);
5625
5755
  return { task, files, createdFiles, success, total, impliesRegistration };
5626
- });
5627
- const layerResults = await Promise.all(taskResultPromises);
5756
+ };
5757
+ const updateCacheFromBatch = async (results) => {
5758
+ for (const result of results) {
5759
+ for (const writtenFile of result.files) {
5760
+ const isCodeFile = /src[\\/](api[s]?|services?|stores?|composables?)[\\/]/i.test(writtenFile);
5761
+ const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
5762
+ if (isCodeFile || isViewFile) {
5763
+ try {
5764
+ const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs8.readFile(path6.join(workingDir, writtenFile), "utf-8");
5765
+ generatedFileCache.set(writtenFile, content);
5766
+ } catch {
5767
+ }
5768
+ }
5769
+ }
5770
+ }
5771
+ };
5772
+ const taskBatches = topoSortLayerTasks(layerTasks);
5773
+ const layerResults = [];
5774
+ for (const batch of taskBatches) {
5775
+ const batchIsParallel = batch.length > 1;
5776
+ const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
5777
+ const batchResults = await Promise.all(batchResultPromises);
5778
+ layerResults.push(...batchResults);
5779
+ await updateCacheFromBatch(batchResults);
5780
+ }
5628
5781
  if (isParallel) {
5629
5782
  console.log("");
5630
5783
  }
@@ -5644,17 +5797,6 @@ ${taskContext}`,
5644
5797
  }
5645
5798
  }
5646
5799
  completedTasks += layerTasks.length;
5647
- for (const result of layerResults) {
5648
- for (const writtenFile of result.files) {
5649
- if (/src[\\/](api[s]?|services?|stores?|composables?)[\\/]/.test(writtenFile)) {
5650
- try {
5651
- const content = await fs8.readFile(path6.join(workingDir, writtenFile), "utf-8");
5652
- generatedFileCache.set(writtenFile, content);
5653
- } catch {
5654
- }
5655
- }
5656
- }
5657
- }
5658
5800
  const anyImpliesRegistration = layerResults.some((r) => r.impliesRegistration);
5659
5801
  if (anyImpliesRegistration && sharedConfigPaths.size > 0 && context?.sharedConfigFiles) {
5660
5802
  const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
@@ -5754,11 +5896,48 @@ ${spec}`,
5754
5896
  console.log(import_chalk6.default.cyan("\n") + plan);
5755
5897
  }
5756
5898
  };
5899
+ function topoSortLayerTasks(tasks) {
5900
+ if (tasks.length <= 1) return [tasks];
5901
+ const idSet = new Set(tasks.map((t) => t.id));
5902
+ const taskById = new Map(tasks.map((t) => [t.id, t]));
5903
+ const inDegree = /* @__PURE__ */ new Map();
5904
+ const dependents = /* @__PURE__ */ new Map();
5905
+ for (const task of tasks) {
5906
+ inDegree.set(task.id, 0);
5907
+ dependents.set(task.id, []);
5908
+ }
5909
+ for (const task of tasks) {
5910
+ const intraDeps = task.dependencies.filter((dep) => idSet.has(dep));
5911
+ inDegree.set(task.id, intraDeps.length);
5912
+ for (const dep of intraDeps) {
5913
+ dependents.get(dep).push(task.id);
5914
+ }
5915
+ }
5916
+ const batches = [];
5917
+ const remaining = new Set(tasks.map((t) => t.id));
5918
+ while (remaining.size > 0) {
5919
+ const batch = [...remaining].filter((id) => inDegree.get(id) === 0).map((id) => taskById.get(id));
5920
+ if (batch.length === 0) {
5921
+ batches.push([...remaining].map((id) => taskById.get(id)));
5922
+ break;
5923
+ }
5924
+ batches.push(batch);
5925
+ for (const task of batch) {
5926
+ remaining.delete(task.id);
5927
+ for (const dependent of dependents.get(task.id)) {
5928
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
5929
+ }
5930
+ }
5931
+ }
5932
+ return batches;
5933
+ }
5757
5934
  var LAYER_ICONS = {
5758
5935
  data: "\u{1F4BE}",
5759
5936
  infra: "\u2699\uFE0F ",
5760
5937
  service: "\u{1F527}",
5761
5938
  api: "\u{1F310}",
5939
+ view: "\u{1F5A5}\uFE0F ",
5940
+ route: "\u{1F5FA}\uFE0F ",
5762
5941
  test: "\u{1F9EA}"
5763
5942
  };
5764
5943
  function printTaskProgress(completed, total, task, mode) {