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/.claude/settings.local.json +1 -6
- package/README.md +76 -10
- package/RELEASE_LOG.md +471 -0
- package/cli/index.ts +201 -23
- package/core/code-generator.ts +269 -32
- package/core/context-loader.ts +1 -1
- package/core/error-feedback.ts +14 -6
- package/core/knowledge-memory.ts +54 -0
- package/core/mock-server-generator.ts +210 -0
- package/core/spec-assessor.ts +1 -1
- package/core/task-generator.ts +7 -3
- package/dist/cli/index.js +553 -57
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +553 -57
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +214 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +214 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/prompts/codegen.prompt.ts +11 -3
- package/prompts/consolidate.prompt.ts +3 -1
- package/prompts/tasks.prompt.ts +28 -5
- package/prompts/update.prompt.ts +1 -1
- package/purpose.md +174 -101
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,
|
|
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.
|
|
4098
|
-
|
|
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. "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5282
|
-
|
|
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
|
|
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
|
|
5588
|
-
const taskResultPromises = layerTasks.map(async (task) => {
|
|
5717
|
+
const executeTask = async (task, batchIsParallel) => {
|
|
5589
5718
|
if (task.filesToTouch.length === 0) {
|
|
5590
|
-
if (!
|
|
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 +
|
|
5749
|
+
constitutionSection + frontendSection + sharedConfigSection + currentGeneratedFilesSection,
|
|
5620
5750
|
systemPrompt,
|
|
5621
|
-
|
|
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
|
|
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) {
|