ai-spec-dev 0.42.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -40
- package/cli/commands/config.ts +129 -1
- package/cli/commands/create.ts +246 -11
- package/cli/commands/fix-history.ts +176 -0
- package/cli/commands/init.ts +344 -106
- package/cli/index.ts +3 -7
- package/cli/pipeline/helpers.ts +6 -0
- package/cli/pipeline/multi-repo.ts +291 -26
- package/cli/pipeline/single-repo.ts +103 -2
- package/cli/utils.ts +95 -4
- package/core/code-generator.ts +63 -14
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/cross-stack-verifier.ts +395 -0
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +3 -2
- package/core/fix-history.ts +333 -0
- package/core/import-fixer.ts +827 -0
- package/core/import-verifier.ts +569 -0
- package/core/knowledge-memory.ts +55 -6
- package/core/openapi-exporter.ts +3 -2
- package/core/repo-store.ts +95 -0
- package/core/reviewer.ts +14 -13
- package/core/run-logger.ts +3 -4
- package/core/run-snapshot.ts +2 -3
- package/core/run-trend.ts +3 -4
- package/core/self-evaluator.ts +44 -7
- package/core/spec-generator.ts +30 -45
- package/core/token-budget.ts +3 -8
- package/core/types-generator.ts +2 -2
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +3889 -1937
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3888 -1936
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +292 -181
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -181
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/cross-stack-verifier.test.ts +301 -0
- package/tests/fix-history.test.ts +335 -0
- package/tests/import-fixer.test.ts +944 -0
- package/tests/import-verifier.test.ts +420 -0
- package/tests/knowledge-memory.test.ts +40 -0
- package/tests/self-evaluator.test.ts +97 -0
- package/cli/commands/model.ts +0 -156
- package/cli/commands/scan.ts +0 -99
- package/cli/commands/workspace.ts +0 -219
- package/demo-backend/.ai-spec-constitution.md +0 -65
- package/demo-backend/package.json +0 -21
- package/demo-backend/prisma/schema.prisma +0 -22
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
- package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
- package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
- package/demo-backend/src/index.ts +0 -17
- package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
- package/demo-backend/src/routes/bookmark.routes.ts +0 -11
- package/demo-backend/src/routes/index.ts +0 -8
- package/demo-backend/src/services/bookmark.service.test.ts +0 -433
- package/demo-backend/src/services/bookmark.service.ts +0 -261
- package/demo-backend/tsconfig.json +0 -12
- package/demo-frontend/.ai-spec-constitution.md +0 -95
- package/demo-frontend/package.json +0 -23
- package/demo-frontend/src/App.tsx +0 -12
- package/demo-frontend/src/main.tsx +0 -9
- package/demo-frontend/tsconfig.json +0 -13
package/dist/index.js
CHANGED
|
@@ -68,7 +68,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
68
68
|
var import_generative_ai = require("@google/generative-ai");
|
|
69
69
|
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
70
70
|
var import_openai = __toESM(require("openai"));
|
|
71
|
-
var import_axios = __toESM(require("axios"));
|
|
72
71
|
var import_undici = require("undici");
|
|
73
72
|
|
|
74
73
|
// prompts/spec.prompt.ts
|
|
@@ -361,7 +360,9 @@ var PROVIDER_CATALOG = {
|
|
|
361
360
|
displayName: "MiMo (Xiaomi)",
|
|
362
361
|
description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
|
|
363
362
|
models: ["mimo-v2-pro"],
|
|
364
|
-
envKey: "MIMO_API_KEY"
|
|
363
|
+
envKey: "MIMO_API_KEY",
|
|
364
|
+
// Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
|
|
365
|
+
fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
|
|
365
366
|
// baseURL not used — MiMo has a dedicated provider class
|
|
366
367
|
},
|
|
367
368
|
gemini: {
|
|
@@ -404,12 +405,12 @@ var PROVIDER_CATALOG = {
|
|
|
404
405
|
},
|
|
405
406
|
deepseek: {
|
|
406
407
|
displayName: "DeepSeek",
|
|
407
|
-
description: "DeepSeek
|
|
408
|
+
description: "DeepSeek V3.2 (chat) / R1 (reasoner) \u2014 alias auto-tracks latest stable",
|
|
408
409
|
models: [
|
|
409
410
|
"deepseek-chat",
|
|
410
|
-
// DeepSeek
|
|
411
|
+
// V3.2 (alias auto-updates as DeepSeek releases new versions)
|
|
411
412
|
"deepseek-reasoner"
|
|
412
|
-
//
|
|
413
|
+
// R1 (reasoning model)
|
|
413
414
|
],
|
|
414
415
|
envKey: "DEEPSEEK_API_KEY",
|
|
415
416
|
baseURL: "https://api.deepseek.com/v1"
|
|
@@ -530,8 +531,8 @@ var ClaudeProvider = class {
|
|
|
530
531
|
...systemInstruction ? { system: systemInstruction } : {},
|
|
531
532
|
messages: [{ role: "user", content: prompt }]
|
|
532
533
|
});
|
|
533
|
-
const
|
|
534
|
-
if (
|
|
534
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
535
|
+
if (textBlock) return textBlock.text;
|
|
535
536
|
throw new Error("Unexpected response type from Claude API");
|
|
536
537
|
},
|
|
537
538
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
@@ -576,43 +577,29 @@ var OpenAICompatibleProvider = class {
|
|
|
576
577
|
}
|
|
577
578
|
};
|
|
578
579
|
var MiMoProvider = class {
|
|
580
|
+
client;
|
|
579
581
|
providerName = "mimo";
|
|
580
582
|
modelName;
|
|
581
|
-
apiKey;
|
|
582
|
-
baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
|
|
583
583
|
constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
|
|
584
|
-
|
|
584
|
+
const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
|
|
585
|
+
this.client = new import_sdk.default({ apiKey, baseURL });
|
|
585
586
|
this.modelName = modelName;
|
|
586
587
|
}
|
|
587
588
|
async generate(prompt, systemInstruction) {
|
|
588
589
|
return withReliability(
|
|
589
590
|
async () => {
|
|
590
|
-
const
|
|
591
|
+
const stream = this.client.messages.stream({
|
|
591
592
|
model: this.modelName,
|
|
592
|
-
max_tokens:
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
stream: false,
|
|
596
|
-
temperature: 1,
|
|
597
|
-
stop_sequences: null
|
|
598
|
-
};
|
|
599
|
-
if (systemInstruction) {
|
|
600
|
-
body.system = systemInstruction;
|
|
601
|
-
}
|
|
602
|
-
const response = await import_axios.default.post(this.baseUrl, body, {
|
|
603
|
-
headers: {
|
|
604
|
-
"api-key": this.apiKey,
|
|
605
|
-
"Content-Type": "application/json"
|
|
606
|
-
}
|
|
593
|
+
max_tokens: 65536,
|
|
594
|
+
...systemInstruction ? { system: systemInstruction } : {},
|
|
595
|
+
messages: [{ role: "user", content: prompt }]
|
|
607
596
|
});
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
|
|
597
|
+
const message = await stream.finalMessage();
|
|
598
|
+
const textBlock = message.content.find((b) => b.type === "text");
|
|
599
|
+
if (textBlock) return textBlock.text;
|
|
600
|
+
const thinkBlock = message.content.find((b) => b.type === "thinking");
|
|
601
|
+
if (thinkBlock) return thinkBlock.thinking;
|
|
602
|
+
return message.content.map((b) => b.text ?? "").join("");
|
|
616
603
|
},
|
|
617
604
|
{ label: `${this.providerName}/${this.modelName}` }
|
|
618
605
|
);
|
|
@@ -4246,8 +4233,8 @@ ${currentSpec}`,
|
|
|
4246
4233
|
// core/code-generator.ts
|
|
4247
4234
|
var import_chalk10 = __toESM(require("chalk"));
|
|
4248
4235
|
var import_child_process2 = require("child_process");
|
|
4249
|
-
var
|
|
4250
|
-
var
|
|
4236
|
+
var path7 = __toESM(require("path"));
|
|
4237
|
+
var fs11 = __toESM(require("fs-extra"));
|
|
4251
4238
|
|
|
4252
4239
|
// prompts/codegen.prompt.ts
|
|
4253
4240
|
var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
|
|
@@ -4917,32 +4904,32 @@ function validateDsl(raw) {
|
|
|
4917
4904
|
}
|
|
4918
4905
|
return { valid: true, dsl: raw };
|
|
4919
4906
|
}
|
|
4920
|
-
function validateFeature(raw,
|
|
4907
|
+
function validateFeature(raw, path11, errors) {
|
|
4921
4908
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4922
|
-
errors.push({ path:
|
|
4909
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4923
4910
|
return;
|
|
4924
4911
|
}
|
|
4925
4912
|
const f = raw;
|
|
4926
|
-
requireNonEmptyString(f["id"], `${
|
|
4927
|
-
requireNonEmptyString(f["title"], `${
|
|
4928
|
-
requireNonEmptyString(f["description"], `${
|
|
4913
|
+
requireNonEmptyString(f["id"], `${path11}.id`, errors);
|
|
4914
|
+
requireNonEmptyString(f["title"], `${path11}.title`, errors);
|
|
4915
|
+
requireNonEmptyString(f["description"], `${path11}.description`, errors);
|
|
4929
4916
|
}
|
|
4930
|
-
function validateModel(raw,
|
|
4917
|
+
function validateModel(raw, path11, errors) {
|
|
4931
4918
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4932
|
-
errors.push({ path:
|
|
4919
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4933
4920
|
return;
|
|
4934
4921
|
}
|
|
4935
4922
|
const m = raw;
|
|
4936
|
-
requireNonEmptyString(m["name"], `${
|
|
4923
|
+
requireNonEmptyString(m["name"], `${path11}.name`, errors);
|
|
4937
4924
|
if (!Array.isArray(m["fields"])) {
|
|
4938
|
-
errors.push({ path: `${
|
|
4925
|
+
errors.push({ path: `${path11}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4939
4926
|
} else {
|
|
4940
4927
|
const fields = m["fields"];
|
|
4941
4928
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4942
|
-
errors.push({ path: `${
|
|
4929
|
+
errors.push({ path: `${path11}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4943
4930
|
}
|
|
4944
4931
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4945
|
-
validateModelField(fields[j2], `${
|
|
4932
|
+
validateModelField(fields[j2], `${path11}.fields[${j2}]`, errors);
|
|
4946
4933
|
}
|
|
4947
4934
|
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
4948
4935
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
@@ -4951,7 +4938,7 @@ function validateModel(raw, path10, errors) {
|
|
|
4951
4938
|
const name = f["name"];
|
|
4952
4939
|
if (seenFieldNames.has(name)) {
|
|
4953
4940
|
errors.push({
|
|
4954
|
-
path: `${
|
|
4941
|
+
path: `${path11}.fields[${j2}].name`,
|
|
4955
4942
|
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
4956
4943
|
});
|
|
4957
4944
|
} else {
|
|
@@ -4962,184 +4949,184 @@ function validateModel(raw, path10, errors) {
|
|
|
4962
4949
|
}
|
|
4963
4950
|
if (m["relations"] !== void 0) {
|
|
4964
4951
|
if (!Array.isArray(m["relations"])) {
|
|
4965
|
-
errors.push({ path: `${
|
|
4952
|
+
errors.push({ path: `${path11}.relations`, message: "Must be an array of strings if present" });
|
|
4966
4953
|
} else {
|
|
4967
4954
|
const rels = m["relations"];
|
|
4968
4955
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4969
4956
|
if (typeof rels[j2] !== "string") {
|
|
4970
|
-
errors.push({ path: `${
|
|
4957
|
+
errors.push({ path: `${path11}.relations[${j2}]`, message: "Must be a string" });
|
|
4971
4958
|
}
|
|
4972
4959
|
}
|
|
4973
4960
|
}
|
|
4974
4961
|
}
|
|
4975
4962
|
}
|
|
4976
|
-
function validateModelField(raw,
|
|
4963
|
+
function validateModelField(raw, path11, errors) {
|
|
4977
4964
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4978
|
-
errors.push({ path:
|
|
4965
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4979
4966
|
return;
|
|
4980
4967
|
}
|
|
4981
4968
|
const f = raw;
|
|
4982
|
-
requireNonEmptyString(f["name"], `${
|
|
4983
|
-
requireNonEmptyString(f["type"], `${
|
|
4969
|
+
requireNonEmptyString(f["name"], `${path11}.name`, errors);
|
|
4970
|
+
requireNonEmptyString(f["type"], `${path11}.type`, errors);
|
|
4984
4971
|
if (typeof f["required"] !== "boolean") {
|
|
4985
|
-
errors.push({ path: `${
|
|
4972
|
+
errors.push({ path: `${path11}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4986
4973
|
}
|
|
4987
4974
|
}
|
|
4988
|
-
function validateEndpoint(raw,
|
|
4975
|
+
function validateEndpoint(raw, path11, errors) {
|
|
4989
4976
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4990
|
-
errors.push({ path:
|
|
4977
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4991
4978
|
return;
|
|
4992
4979
|
}
|
|
4993
4980
|
const e = raw;
|
|
4994
|
-
requireNonEmptyString(e["id"], `${
|
|
4995
|
-
requireNonEmptyString(e["description"], `${
|
|
4981
|
+
requireNonEmptyString(e["id"], `${path11}.id`, errors);
|
|
4982
|
+
requireNonEmptyString(e["description"], `${path11}.description`, errors);
|
|
4996
4983
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4997
4984
|
errors.push({
|
|
4998
|
-
path: `${
|
|
4985
|
+
path: `${path11}.method`,
|
|
4999
4986
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
5000
4987
|
});
|
|
5001
4988
|
}
|
|
5002
4989
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
5003
4990
|
errors.push({
|
|
5004
|
-
path: `${
|
|
4991
|
+
path: `${path11}.path`,
|
|
5005
4992
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
5006
4993
|
});
|
|
5007
4994
|
}
|
|
5008
4995
|
if (typeof e["auth"] !== "boolean") {
|
|
5009
|
-
errors.push({ path: `${
|
|
4996
|
+
errors.push({ path: `${path11}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
5010
4997
|
}
|
|
5011
4998
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
5012
4999
|
errors.push({
|
|
5013
|
-
path: `${
|
|
5000
|
+
path: `${path11}.successStatus`,
|
|
5014
5001
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
5015
5002
|
});
|
|
5016
5003
|
}
|
|
5017
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
5004
|
+
requireNonEmptyString(e["successDescription"], `${path11}.successDescription`, errors);
|
|
5018
5005
|
if (e["request"] !== void 0) {
|
|
5019
|
-
validateRequestSchema(e["request"], `${
|
|
5006
|
+
validateRequestSchema(e["request"], `${path11}.request`, errors);
|
|
5020
5007
|
}
|
|
5021
5008
|
if (e["errors"] !== void 0) {
|
|
5022
5009
|
if (!Array.isArray(e["errors"])) {
|
|
5023
|
-
errors.push({ path: `${
|
|
5010
|
+
errors.push({ path: `${path11}.errors`, message: "Must be an array if present" });
|
|
5024
5011
|
} else {
|
|
5025
5012
|
const errs = e["errors"];
|
|
5026
5013
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
5027
|
-
errors.push({ path: `${
|
|
5014
|
+
errors.push({ path: `${path11}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
5028
5015
|
}
|
|
5029
5016
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
5030
|
-
validateResponseError(errs[j2], `${
|
|
5017
|
+
validateResponseError(errs[j2], `${path11}.errors[${j2}]`, errors);
|
|
5031
5018
|
}
|
|
5032
5019
|
}
|
|
5033
5020
|
}
|
|
5034
5021
|
}
|
|
5035
|
-
function validateRequestSchema(raw,
|
|
5022
|
+
function validateRequestSchema(raw, path11, errors) {
|
|
5036
5023
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5037
|
-
errors.push({ path:
|
|
5024
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5038
5025
|
return;
|
|
5039
5026
|
}
|
|
5040
5027
|
const r = raw;
|
|
5041
5028
|
for (const key of ["body", "query", "params"]) {
|
|
5042
5029
|
if (r[key] !== void 0) {
|
|
5043
|
-
validateFieldMap(r[key], `${
|
|
5030
|
+
validateFieldMap(r[key], `${path11}.${key}`, errors);
|
|
5044
5031
|
}
|
|
5045
5032
|
}
|
|
5046
5033
|
}
|
|
5047
|
-
function validateFieldMap(raw,
|
|
5034
|
+
function validateFieldMap(raw, path11, errors) {
|
|
5048
5035
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5049
|
-
errors.push({ path:
|
|
5036
|
+
errors.push({ path: path11, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
5050
5037
|
return;
|
|
5051
5038
|
}
|
|
5052
5039
|
const map = raw;
|
|
5053
5040
|
for (const [k2, v2] of Object.entries(map)) {
|
|
5054
5041
|
if (typeof v2 !== "string") {
|
|
5055
|
-
errors.push({ path: `${
|
|
5042
|
+
errors.push({ path: `${path11}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
5056
5043
|
}
|
|
5057
5044
|
}
|
|
5058
5045
|
}
|
|
5059
|
-
function validateResponseError(raw,
|
|
5046
|
+
function validateResponseError(raw, path11, errors) {
|
|
5060
5047
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5061
|
-
errors.push({ path:
|
|
5048
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5062
5049
|
return;
|
|
5063
5050
|
}
|
|
5064
5051
|
const e = raw;
|
|
5065
5052
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
5066
|
-
errors.push({ path: `${
|
|
5053
|
+
errors.push({ path: `${path11}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
5067
5054
|
}
|
|
5068
|
-
requireNonEmptyString(e["code"], `${
|
|
5069
|
-
requireNonEmptyString(e["description"], `${
|
|
5055
|
+
requireNonEmptyString(e["code"], `${path11}.code`, errors);
|
|
5056
|
+
requireNonEmptyString(e["description"], `${path11}.description`, errors);
|
|
5070
5057
|
}
|
|
5071
|
-
function validateBehavior(raw,
|
|
5058
|
+
function validateBehavior(raw, path11, errors) {
|
|
5072
5059
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5073
|
-
errors.push({ path:
|
|
5060
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5074
5061
|
return;
|
|
5075
5062
|
}
|
|
5076
5063
|
const b = raw;
|
|
5077
|
-
requireNonEmptyString(b["id"], `${
|
|
5078
|
-
requireNonEmptyString(b["description"], `${
|
|
5064
|
+
requireNonEmptyString(b["id"], `${path11}.id`, errors);
|
|
5065
|
+
requireNonEmptyString(b["description"], `${path11}.description`, errors);
|
|
5079
5066
|
if (b["constraints"] !== void 0) {
|
|
5080
5067
|
if (!Array.isArray(b["constraints"])) {
|
|
5081
|
-
errors.push({ path: `${
|
|
5068
|
+
errors.push({ path: `${path11}.constraints`, message: "Must be an array of strings if present" });
|
|
5082
5069
|
} else {
|
|
5083
5070
|
const cs2 = b["constraints"];
|
|
5084
5071
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5085
5072
|
if (typeof cs2[j2] !== "string") {
|
|
5086
|
-
errors.push({ path: `${
|
|
5073
|
+
errors.push({ path: `${path11}.constraints[${j2}]`, message: "Must be a string" });
|
|
5087
5074
|
}
|
|
5088
5075
|
}
|
|
5089
5076
|
}
|
|
5090
5077
|
}
|
|
5091
5078
|
}
|
|
5092
|
-
function validateComponent(raw,
|
|
5079
|
+
function validateComponent(raw, path11, errors) {
|
|
5093
5080
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5094
|
-
errors.push({ path:
|
|
5081
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5095
5082
|
return;
|
|
5096
5083
|
}
|
|
5097
5084
|
const c = raw;
|
|
5098
|
-
requireNonEmptyString(c["id"], `${
|
|
5099
|
-
requireNonEmptyString(c["name"], `${
|
|
5100
|
-
requireNonEmptyString(c["description"], `${
|
|
5085
|
+
requireNonEmptyString(c["id"], `${path11}.id`, errors);
|
|
5086
|
+
requireNonEmptyString(c["name"], `${path11}.name`, errors);
|
|
5087
|
+
requireNonEmptyString(c["description"], `${path11}.description`, errors);
|
|
5101
5088
|
if (c["props"] !== void 0) {
|
|
5102
5089
|
if (!Array.isArray(c["props"])) {
|
|
5103
|
-
errors.push({ path: `${
|
|
5090
|
+
errors.push({ path: `${path11}.props`, message: "Must be an array if present" });
|
|
5104
5091
|
} else {
|
|
5105
5092
|
const props = c["props"];
|
|
5106
5093
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5107
5094
|
const p = props[j2];
|
|
5108
5095
|
if (typeof p !== "object" || p === null) {
|
|
5109
|
-
errors.push({ path: `${
|
|
5096
|
+
errors.push({ path: `${path11}.props[${j2}]`, message: "Must be an object" });
|
|
5110
5097
|
continue;
|
|
5111
5098
|
}
|
|
5112
|
-
requireNonEmptyString(p["name"], `${
|
|
5113
|
-
requireNonEmptyString(p["type"], `${
|
|
5099
|
+
requireNonEmptyString(p["name"], `${path11}.props[${j2}].name`, errors);
|
|
5100
|
+
requireNonEmptyString(p["type"], `${path11}.props[${j2}].type`, errors);
|
|
5114
5101
|
if (typeof p["required"] !== "boolean") {
|
|
5115
|
-
errors.push({ path: `${
|
|
5102
|
+
errors.push({ path: `${path11}.props[${j2}].required`, message: "Must be boolean" });
|
|
5116
5103
|
}
|
|
5117
5104
|
}
|
|
5118
5105
|
}
|
|
5119
5106
|
}
|
|
5120
5107
|
if (c["events"] !== void 0) {
|
|
5121
5108
|
if (!Array.isArray(c["events"])) {
|
|
5122
|
-
errors.push({ path: `${
|
|
5109
|
+
errors.push({ path: `${path11}.events`, message: "Must be an array if present" });
|
|
5123
5110
|
} else {
|
|
5124
5111
|
const events = c["events"];
|
|
5125
5112
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5126
5113
|
const e = events[j2];
|
|
5127
5114
|
if (typeof e !== "object" || e === null) {
|
|
5128
|
-
errors.push({ path: `${
|
|
5115
|
+
errors.push({ path: `${path11}.events[${j2}]`, message: "Must be an object" });
|
|
5129
5116
|
continue;
|
|
5130
5117
|
}
|
|
5131
|
-
requireNonEmptyString(e["name"], `${
|
|
5118
|
+
requireNonEmptyString(e["name"], `${path11}.events[${j2}].name`, errors);
|
|
5132
5119
|
}
|
|
5133
5120
|
}
|
|
5134
5121
|
}
|
|
5135
5122
|
if (c["state"] !== void 0) {
|
|
5136
5123
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5137
|
-
errors.push({ path: `${
|
|
5124
|
+
errors.push({ path: `${path11}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5138
5125
|
}
|
|
5139
5126
|
}
|
|
5140
5127
|
if (c["apiCalls"] !== void 0) {
|
|
5141
5128
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5142
|
-
errors.push({ path: `${
|
|
5129
|
+
errors.push({ path: `${path11}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5143
5130
|
}
|
|
5144
5131
|
}
|
|
5145
5132
|
}
|
|
@@ -5201,10 +5188,10 @@ function crossReferenceChecks(obj, errors) {
|
|
|
5201
5188
|
}
|
|
5202
5189
|
}
|
|
5203
5190
|
}
|
|
5204
|
-
function requireNonEmptyString(v2,
|
|
5191
|
+
function requireNonEmptyString(v2, path11, errors) {
|
|
5205
5192
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5206
5193
|
errors.push({
|
|
5207
|
-
path:
|
|
5194
|
+
path: path11,
|
|
5208
5195
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5209
5196
|
});
|
|
5210
5197
|
}
|
|
@@ -5217,13 +5204,11 @@ function typeLabel(v2) {
|
|
|
5217
5204
|
|
|
5218
5205
|
// core/token-budget.ts
|
|
5219
5206
|
var import_chalk6 = __toESM(require("chalk"));
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5226
|
-
}
|
|
5207
|
+
|
|
5208
|
+
// core/config-defaults.ts
|
|
5209
|
+
var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
|
|
5210
|
+
var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
|
|
5211
|
+
var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
|
|
5227
5212
|
var DEFAULT_TOKEN_BUDGETS = {
|
|
5228
5213
|
gemini: 9e5,
|
|
5229
5214
|
claude: 18e4,
|
|
@@ -5231,8 +5216,18 @@ var DEFAULT_TOKEN_BUDGETS = {
|
|
|
5231
5216
|
deepseek: 6e4,
|
|
5232
5217
|
default: 1e5
|
|
5233
5218
|
};
|
|
5219
|
+
|
|
5220
|
+
// core/token-budget.ts
|
|
5221
|
+
var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
|
|
5222
|
+
function estimateTokens(text) {
|
|
5223
|
+
if (!text) return 0;
|
|
5224
|
+
const cjkCount = (text.match(CJK_RANGE) ?? []).length;
|
|
5225
|
+
const nonCjkLength = text.length - cjkCount;
|
|
5226
|
+
return Math.ceil(cjkCount + nonCjkLength / 4);
|
|
5227
|
+
}
|
|
5228
|
+
var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
|
|
5234
5229
|
function getDefaultBudget(providerName) {
|
|
5235
|
-
return
|
|
5230
|
+
return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
|
|
5236
5231
|
}
|
|
5237
5232
|
|
|
5238
5233
|
// core/dsl-extractor.ts
|
|
@@ -6085,6 +6080,101 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6085
6080
|
}
|
|
6086
6081
|
}
|
|
6087
6082
|
|
|
6083
|
+
// core/fix-history.ts
|
|
6084
|
+
var fs10 = __toESM(require("fs-extra"));
|
|
6085
|
+
var path6 = __toESM(require("path"));
|
|
6086
|
+
var FIX_HISTORY_FILE = ".ai-spec-fix-history.json";
|
|
6087
|
+
var FIX_HISTORY_VERSION = "1.0";
|
|
6088
|
+
async function loadFixHistory(repoRoot) {
|
|
6089
|
+
const filePath = path6.join(repoRoot, FIX_HISTORY_FILE);
|
|
6090
|
+
if (!await fs10.pathExists(filePath)) {
|
|
6091
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6092
|
+
}
|
|
6093
|
+
try {
|
|
6094
|
+
const data = await fs10.readJson(filePath);
|
|
6095
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.entries)) {
|
|
6096
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6097
|
+
}
|
|
6098
|
+
return {
|
|
6099
|
+
version: typeof data.version === "string" ? data.version : FIX_HISTORY_VERSION,
|
|
6100
|
+
entries: data.entries
|
|
6101
|
+
};
|
|
6102
|
+
} catch {
|
|
6103
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
function aggregateFixPatterns(history) {
|
|
6107
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6108
|
+
for (const entry of history.entries) {
|
|
6109
|
+
if (!byKey.has(entry.patternKey)) byKey.set(entry.patternKey, []);
|
|
6110
|
+
byKey.get(entry.patternKey).push(entry);
|
|
6111
|
+
}
|
|
6112
|
+
const aggregates = [];
|
|
6113
|
+
for (const [patternKey, entries] of byKey) {
|
|
6114
|
+
entries.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
6115
|
+
const first = entries[0];
|
|
6116
|
+
const last = entries[entries.length - 1];
|
|
6117
|
+
const uniqueRunIds = new Set(entries.map((e) => e.runId)).size;
|
|
6118
|
+
aggregates.push({
|
|
6119
|
+
patternKey,
|
|
6120
|
+
count: entries.length,
|
|
6121
|
+
firstSeen: first.ts,
|
|
6122
|
+
lastSeen: last.ts,
|
|
6123
|
+
uniqueRunIds,
|
|
6124
|
+
source: last.brokenImport.source,
|
|
6125
|
+
names: last.brokenImport.names,
|
|
6126
|
+
reason: last.brokenImport.reason,
|
|
6127
|
+
fix: {
|
|
6128
|
+
kind: last.fix.kind,
|
|
6129
|
+
target: last.fix.target,
|
|
6130
|
+
stage: last.fix.stage
|
|
6131
|
+
}
|
|
6132
|
+
});
|
|
6133
|
+
}
|
|
6134
|
+
aggregates.sort((a, b) => {
|
|
6135
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
6136
|
+
return b.lastSeen.localeCompare(a.lastSeen);
|
|
6137
|
+
});
|
|
6138
|
+
return aggregates;
|
|
6139
|
+
}
|
|
6140
|
+
function buildHallucinationAvoidanceSection(history, opts = {}) {
|
|
6141
|
+
const minCount = opts.minCount ?? 1;
|
|
6142
|
+
const maxItems = opts.maxItems ?? 10;
|
|
6143
|
+
const patterns = aggregateFixPatterns(history).filter((p) => p.count >= minCount);
|
|
6144
|
+
if (patterns.length === 0) return null;
|
|
6145
|
+
const top = patterns.slice(0, maxItems);
|
|
6146
|
+
const lines = [
|
|
6147
|
+
"=== Prior Hallucinations in This Project (DO NOT REPEAT) ===",
|
|
6148
|
+
"",
|
|
6149
|
+
"The following imports were previously hallucinated by AI codegen in this",
|
|
6150
|
+
"project and had to be auto-fixed. When generating new files, actively avoid",
|
|
6151
|
+
"these exact imports \u2014 they were wrong in the past and will be wrong again.",
|
|
6152
|
+
""
|
|
6153
|
+
];
|
|
6154
|
+
for (const p of top) {
|
|
6155
|
+
const namesLabel = p.names.length > 0 ? `{ ${p.names.join(", ")} }` : "(no names)";
|
|
6156
|
+
const reasonLabel = p.reason === "file_not_found" ? "file did not exist" : "named export did not exist";
|
|
6157
|
+
const countLabel = p.count === 1 ? "1x" : `${p.count}x`;
|
|
6158
|
+
const dateLabel = p.lastSeen.slice(0, 10);
|
|
6159
|
+
lines.push(`\u274C Do NOT: import ${namesLabel} from '${p.source}'`);
|
|
6160
|
+
lines.push(` Reason: ${reasonLabel} (seen ${countLabel}, last ${dateLabel})`);
|
|
6161
|
+
if (p.fix.kind === "create_file") {
|
|
6162
|
+
lines.push(` Previously fixed by creating: ${p.fix.target}`);
|
|
6163
|
+
} else if (p.fix.kind === "rewrite_import") {
|
|
6164
|
+
lines.push(` Previously fixed by rewriting the import path`);
|
|
6165
|
+
} else {
|
|
6166
|
+
lines.push(` Previously fixed by appending to: ${p.fix.target}`);
|
|
6167
|
+
}
|
|
6168
|
+
lines.push("");
|
|
6169
|
+
}
|
|
6170
|
+
if (patterns.length > maxItems) {
|
|
6171
|
+
lines.push(`(${patterns.length - maxItems} more pattern(s) hidden \u2014 run \`ai-spec fix-history\` to see all)`);
|
|
6172
|
+
lines.push("");
|
|
6173
|
+
}
|
|
6174
|
+
lines.push("=== End of Prior Hallucinations ===");
|
|
6175
|
+
return lines.join("\n");
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6088
6178
|
// core/code-generator.ts
|
|
6089
6179
|
var CodeGenerator = class {
|
|
6090
6180
|
constructor(provider, mode = "claude-code") {
|
|
@@ -6149,8 +6239,8 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
6149
6239
|
Files: ${t.filesToTouch.join(", ")}
|
|
6150
6240
|
Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
|
|
6151
6241
|
const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
|
|
6152
|
-
const promptFile =
|
|
6153
|
-
await
|
|
6242
|
+
const promptFile = path7.join(workingDir, ".claude-prompt.txt");
|
|
6243
|
+
await fs11.writeFile(promptFile, promptContent, "utf-8");
|
|
6154
6244
|
if (options.auto) {
|
|
6155
6245
|
console.log(import_chalk10.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6156
6246
|
console.log(import_chalk10.default.gray(` Spec: ${specFilePath}`));
|
|
@@ -6243,7 +6333,7 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
6243
6333
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
6244
6334
|
console.log(import_chalk10.default.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
6245
6335
|
}
|
|
6246
|
-
const spec = await
|
|
6336
|
+
const spec = await fs11.readFile(specFilePath, "utf-8");
|
|
6247
6337
|
let constitutionSection = context?.constitution ? `
|
|
6248
6338
|
=== Project Constitution (MUST follow) ===
|
|
6249
6339
|
${context.constitution}
|
|
@@ -6270,7 +6360,24 @@ ${buildFrontendContextSection(fctx)}
|
|
|
6270
6360
|
`;
|
|
6271
6361
|
console.log(import_chalk10.default.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
6272
6362
|
}
|
|
6273
|
-
|
|
6363
|
+
let fixHistorySection = "";
|
|
6364
|
+
if (options.injectFixHistory !== false) {
|
|
6365
|
+
try {
|
|
6366
|
+
const history = await loadFixHistory(workingDir);
|
|
6367
|
+
const section = buildHallucinationAvoidanceSection(history, {
|
|
6368
|
+
maxItems: options.fixHistoryInjectMax ?? 10
|
|
6369
|
+
});
|
|
6370
|
+
if (section) {
|
|
6371
|
+
fixHistorySection = `
|
|
6372
|
+
${section}
|
|
6373
|
+
`;
|
|
6374
|
+
const patternCount = (section.match(/❌ Do NOT/g) ?? []).length;
|
|
6375
|
+
console.log(import_chalk10.default.cyan(` \u2713 Injected ${patternCount} prior hallucination pattern(s) from fix-history`));
|
|
6376
|
+
}
|
|
6377
|
+
} catch {
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection + fixHistorySection;
|
|
6274
6381
|
const estimatedTokenCount = estimateTokens(allContextText);
|
|
6275
6382
|
const budget = getDefaultBudget(this.provider.providerName);
|
|
6276
6383
|
if (estimatedTokenCount > budget * 0.7) {
|
|
@@ -6289,7 +6396,7 @@ ${buildFrontendContextSection(fctx)}
|
|
|
6289
6396
|
}
|
|
6290
6397
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
6291
6398
|
if (tasks && tasks.length > 0) {
|
|
6292
|
-
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6399
|
+
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection + fixHistorySection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6293
6400
|
}
|
|
6294
6401
|
console.log(import_chalk10.default.gray(" [1/2] Planning implementation files..."));
|
|
6295
6402
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
@@ -6300,7 +6407,7 @@ IMPORTANT: Check the "Frontend Project Context" section below. Extend existing h
|
|
|
6300
6407
|
|
|
6301
6408
|
=== Feature Spec ===
|
|
6302
6409
|
${spec}
|
|
6303
|
-
${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
|
|
6410
|
+
${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}${fixHistorySection}
|
|
6304
6411
|
=== Project Context ===
|
|
6305
6412
|
${contextSummary}
|
|
6306
6413
|
|
|
@@ -6327,7 +6434,7 @@ Output ONLY a valid JSON array:
|
|
|
6327
6434
|
const icon = item.action === "create" ? import_chalk10.default.green("+") : import_chalk10.default.yellow("~");
|
|
6328
6435
|
console.log(` ${icon} ${item.file}: ${import_chalk10.default.gray(item.description)}`);
|
|
6329
6436
|
});
|
|
6330
|
-
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
6437
|
+
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection + fixHistorySection, systemPrompt);
|
|
6331
6438
|
return files;
|
|
6332
6439
|
}
|
|
6333
6440
|
async runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection, frontendSection = "", sharedConfigSection = "", options = {}, systemPrompt = getCodeGenSystemPrompt(), context) {
|
|
@@ -6387,7 +6494,7 @@ Output ONLY a valid JSON array:
|
|
|
6387
6494
|
}
|
|
6388
6495
|
const filePlan = await Promise.all(
|
|
6389
6496
|
task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
|
|
6390
|
-
const exists = await
|
|
6497
|
+
const exists = await fs11.pathExists(path7.join(workingDir, f));
|
|
6391
6498
|
return {
|
|
6392
6499
|
file: f,
|
|
6393
6500
|
action: exists ? "modify" : "create",
|
|
@@ -6427,7 +6534,7 @@ ${taskContext}`,
|
|
|
6427
6534
|
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
|
|
6428
6535
|
if (isCodeFile || isViewFile) {
|
|
6429
6536
|
try {
|
|
6430
|
-
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await
|
|
6537
|
+
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs11.readFile(path7.join(workingDir, writtenFile), "utf-8");
|
|
6431
6538
|
generatedFileCache.set(writtenFile, content);
|
|
6432
6539
|
} catch {
|
|
6433
6540
|
}
|
|
@@ -6437,19 +6544,28 @@ ${taskContext}`,
|
|
|
6437
6544
|
};
|
|
6438
6545
|
const taskBatches = topoSortLayerTasks(layerTasks);
|
|
6439
6546
|
const layerResults = [];
|
|
6547
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? 3);
|
|
6440
6548
|
for (const batch of taskBatches) {
|
|
6441
6549
|
const batchIsParallel = batch.length > 1;
|
|
6442
|
-
const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
|
|
6443
|
-
const settled = await Promise.allSettled(batchResultPromises);
|
|
6444
6550
|
const batchResults = [];
|
|
6445
|
-
for (let
|
|
6446
|
-
const
|
|
6447
|
-
if (
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6551
|
+
for (let chunkStart = 0; chunkStart < batch.length; chunkStart += maxConcurrency) {
|
|
6552
|
+
const chunk = batch.slice(chunkStart, chunkStart + maxConcurrency);
|
|
6553
|
+
if (batchIsParallel && batch.length > maxConcurrency) {
|
|
6554
|
+
const chunkIdx = Math.floor(chunkStart / maxConcurrency) + 1;
|
|
6555
|
+
const totalChunks = Math.ceil(batch.length / maxConcurrency);
|
|
6556
|
+
console.log(import_chalk10.default.gray(` \u21B3 chunk ${chunkIdx}/${totalChunks} (${chunk.length} tasks, concurrency cap: ${maxConcurrency})`));
|
|
6557
|
+
}
|
|
6558
|
+
const chunkResultPromises = chunk.map((task) => executeTask(task, batchIsParallel));
|
|
6559
|
+
const settled = await Promise.allSettled(chunkResultPromises);
|
|
6560
|
+
for (let i = 0; i < settled.length; i++) {
|
|
6561
|
+
const outcome = settled[i];
|
|
6562
|
+
if (outcome.status === "fulfilled") {
|
|
6563
|
+
batchResults.push(outcome.value);
|
|
6564
|
+
} else {
|
|
6565
|
+
const task = chunk[i];
|
|
6566
|
+
console.log(import_chalk10.default.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
|
|
6567
|
+
batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
|
|
6568
|
+
}
|
|
6453
6569
|
}
|
|
6454
6570
|
}
|
|
6455
6571
|
layerResults.push(...batchResults);
|
|
@@ -6479,7 +6595,7 @@ ${taskContext}`,
|
|
|
6479
6595
|
const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
|
|
6480
6596
|
for (const sharedFile of context.sharedConfigFiles) {
|
|
6481
6597
|
if (processedSharedConfigs.has(sharedFile.path)) continue;
|
|
6482
|
-
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) =>
|
|
6598
|
+
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path7.basename(f).replace(/\.[jt]sx?$/, ""));
|
|
6483
6599
|
if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
|
|
6484
6600
|
let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
|
|
6485
6601
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
@@ -6519,10 +6635,10 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
6519
6635
|
let successCount = 0;
|
|
6520
6636
|
const writtenFiles = [];
|
|
6521
6637
|
for (const item of filePlan) {
|
|
6522
|
-
const fullPath =
|
|
6638
|
+
const fullPath = path7.join(workingDir, item.file);
|
|
6523
6639
|
let existingContent = "";
|
|
6524
|
-
if (await
|
|
6525
|
-
existingContent = await
|
|
6640
|
+
if (await fs11.pathExists(fullPath)) {
|
|
6641
|
+
existingContent = await fs11.readFile(fullPath, "utf-8");
|
|
6526
6642
|
}
|
|
6527
6643
|
const codePrompt = `Implement this file.
|
|
6528
6644
|
|
|
@@ -6539,8 +6655,8 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6539
6655
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
6540
6656
|
const fileContent = stripCodeFences(raw);
|
|
6541
6657
|
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
6542
|
-
await
|
|
6543
|
-
await
|
|
6658
|
+
await fs11.ensureDir(path7.dirname(fullPath));
|
|
6659
|
+
await fs11.writeFile(fullPath, fileContent, "utf-8");
|
|
6544
6660
|
getActiveLogger()?.fileWritten(item.file);
|
|
6545
6661
|
fileSpinner.succeed(`${existingContent ? import_chalk10.default.yellow("~") : import_chalk10.default.green("+")} ${import_chalk10.default.bold(item.file)}`);
|
|
6546
6662
|
successCount++;
|
|
@@ -6561,7 +6677,7 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6561
6677
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
6562
6678
|
async runPlanMode(specFilePath) {
|
|
6563
6679
|
console.log(import_chalk10.default.blue("\n\u2500\u2500\u2500 Implementation Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
6564
|
-
const spec = await
|
|
6680
|
+
const spec = await fs11.readFile(specFilePath, "utf-8");
|
|
6565
6681
|
const plan = await this.provider.generate(
|
|
6566
6682
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
6567
6683
|
Be specific about:
|
|
@@ -6580,13 +6696,13 @@ ${spec}`,
|
|
|
6580
6696
|
// core/reviewer.ts
|
|
6581
6697
|
var import_chalk12 = __toESM(require("chalk"));
|
|
6582
6698
|
var import_child_process3 = require("child_process");
|
|
6583
|
-
var
|
|
6584
|
-
var
|
|
6699
|
+
var path9 = __toESM(require("path"));
|
|
6700
|
+
var fs13 = __toESM(require("fs-extra"));
|
|
6585
6701
|
|
|
6586
6702
|
// core/constitution-generator.ts
|
|
6587
6703
|
var import_chalk11 = __toESM(require("chalk"));
|
|
6588
|
-
var
|
|
6589
|
-
var
|
|
6704
|
+
var fs12 = __toESM(require("fs-extra"));
|
|
6705
|
+
var path8 = __toESM(require("path"));
|
|
6590
6706
|
|
|
6591
6707
|
// prompts/constitution.prompt.ts
|
|
6592
6708
|
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.
|
|
@@ -6666,8 +6782,8 @@ var ConstitutionGenerator = class {
|
|
|
6666
6782
|
return this.provider.generate(prompt, constitutionSystemPrompt);
|
|
6667
6783
|
}
|
|
6668
6784
|
async saveConstitution(projectRoot, content) {
|
|
6669
|
-
const filePath =
|
|
6670
|
-
await
|
|
6785
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6786
|
+
await fs12.writeFile(filePath, content, "utf-8");
|
|
6671
6787
|
return filePath;
|
|
6672
6788
|
}
|
|
6673
6789
|
};
|
|
@@ -6693,7 +6809,7 @@ ${context.routeSummary}
|
|
|
6693
6809
|
}
|
|
6694
6810
|
if (context.schema) {
|
|
6695
6811
|
parts.push(`=== Prisma Schema ===
|
|
6696
|
-
${context.schema.slice(0,
|
|
6812
|
+
${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
|
|
6697
6813
|
`);
|
|
6698
6814
|
}
|
|
6699
6815
|
if (context.errorPatterns) {
|
|
@@ -6725,9 +6841,9 @@ ${sections.join("\n")}
|
|
|
6725
6841
|
return parts.join("\n");
|
|
6726
6842
|
}
|
|
6727
6843
|
async function loadConstitution(projectRoot) {
|
|
6728
|
-
const filePath =
|
|
6729
|
-
if (await
|
|
6730
|
-
return
|
|
6844
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6845
|
+
if (await fs12.pathExists(filePath)) {
|
|
6846
|
+
return fs12.readFile(filePath, "utf-8");
|
|
6731
6847
|
}
|
|
6732
6848
|
return void 0;
|
|
6733
6849
|
}
|
|
@@ -6743,10 +6859,10 @@ function printConstitutionHint(exists) {
|
|
|
6743
6859
|
|
|
6744
6860
|
// core/reviewer.ts
|
|
6745
6861
|
async function loadAccumulatedLessons(projectRoot) {
|
|
6746
|
-
const constitutionPath =
|
|
6862
|
+
const constitutionPath = path9.join(projectRoot, CONSTITUTION_FILE);
|
|
6747
6863
|
let content;
|
|
6748
6864
|
try {
|
|
6749
|
-
content = await
|
|
6865
|
+
content = await fs13.readFile(constitutionPath, "utf-8");
|
|
6750
6866
|
} catch {
|
|
6751
6867
|
return null;
|
|
6752
6868
|
}
|
|
@@ -6757,23 +6873,23 @@ async function loadAccumulatedLessons(projectRoot) {
|
|
|
6757
6873
|
const nextSection = section.slice(marker.length).match(/\n## \d/);
|
|
6758
6874
|
return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
|
|
6759
6875
|
}
|
|
6760
|
-
var REVIEW_HISTORY_FILE =
|
|
6876
|
+
var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
|
|
6761
6877
|
async function loadReviewHistory(projectRoot) {
|
|
6762
|
-
const historyPath =
|
|
6878
|
+
const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6763
6879
|
try {
|
|
6764
|
-
if (await
|
|
6765
|
-
return await
|
|
6880
|
+
if (await fs13.pathExists(historyPath)) {
|
|
6881
|
+
return await fs13.readJson(historyPath);
|
|
6766
6882
|
}
|
|
6767
6883
|
} catch {
|
|
6768
6884
|
}
|
|
6769
6885
|
return [];
|
|
6770
6886
|
}
|
|
6771
6887
|
async function appendReviewHistory(projectRoot, entry) {
|
|
6772
|
-
const historyPath =
|
|
6888
|
+
const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6773
6889
|
const existing = await loadReviewHistory(projectRoot);
|
|
6774
6890
|
const updated = [...existing, entry].slice(-20);
|
|
6775
6891
|
try {
|
|
6776
|
-
await
|
|
6892
|
+
await fs13.writeJson(historyPath, updated, { spaces: 2 });
|
|
6777
6893
|
} catch {
|
|
6778
6894
|
}
|
|
6779
6895
|
}
|
|
@@ -6807,7 +6923,7 @@ function buildHistoryContext(history) {
|
|
|
6807
6923
|
const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
|
|
6808
6924
|
for (const entry of recent) {
|
|
6809
6925
|
lines.push(`
|
|
6810
|
-
[${entry.date}] ${
|
|
6926
|
+
[${entry.date}] ${path9.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
|
|
6811
6927
|
entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
|
|
6812
6928
|
}
|
|
6813
6929
|
return lines.join("\n") + "\n";
|
|
@@ -6888,15 +7004,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
|
|
|
6888
7004
|
${codeContext}`;
|
|
6889
7005
|
const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
|
|
6890
7006
|
console.log(import_chalk12.default.gray(" Pass 2/3: Implementation review..."));
|
|
7007
|
+
const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
|
|
6891
7008
|
const history = await loadReviewHistory(this.projectRoot);
|
|
6892
7009
|
const historyContext = buildHistoryContext(history);
|
|
6893
7010
|
const implPrompt = `Review the implementation details of this change.
|
|
6894
7011
|
|
|
6895
|
-
=== Feature Spec ===
|
|
6896
|
-
${
|
|
6897
|
-
|
|
6898
|
-
=== Code ===
|
|
6899
|
-
${codeContext}
|
|
7012
|
+
=== Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
|
|
7013
|
+
${specDigest}
|
|
6900
7014
|
|
|
6901
7015
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
|
|
6902
7016
|
${archReview}
|
|
@@ -6905,11 +7019,8 @@ ${historyContext}`;
|
|
|
6905
7019
|
console.log(import_chalk12.default.gray(" Pass 3/3: Impact & complexity assessment..."));
|
|
6906
7020
|
const impactPrompt = `Assess the impact and complexity of this change.
|
|
6907
7021
|
|
|
6908
|
-
=== Feature Spec ===
|
|
6909
|
-
${
|
|
6910
|
-
|
|
6911
|
-
=== Code ===
|
|
6912
|
-
${codeContext}
|
|
7022
|
+
=== Feature Spec (digest) ===
|
|
7023
|
+
${specDigest}
|
|
6913
7024
|
|
|
6914
7025
|
=== Architecture Review (Pass 1 \u2014 do NOT repeat) ===
|
|
6915
7026
|
${archReview}
|
|
@@ -6932,7 +7043,7 @@ ${sep}
|
|
|
6932
7043
|
if (score > 0 && specFile) {
|
|
6933
7044
|
await appendReviewHistory(this.projectRoot, {
|
|
6934
7045
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
6935
|
-
specFile:
|
|
7046
|
+
specFile: path9.relative(this.projectRoot, specFile),
|
|
6936
7047
|
score,
|
|
6937
7048
|
...complianceScore > 0 ? { complianceScore } : {},
|
|
6938
7049
|
topIssues,
|
|
@@ -6977,14 +7088,14 @@ ${sep}
|
|
|
6977
7088
|
);
|
|
6978
7089
|
let filesSection = "";
|
|
6979
7090
|
for (const filePath of filePaths) {
|
|
6980
|
-
const fullPath =
|
|
7091
|
+
const fullPath = path9.join(workingDir, filePath);
|
|
6981
7092
|
try {
|
|
6982
|
-
const content = await
|
|
7093
|
+
const content = await fs13.readFile(fullPath, "utf-8");
|
|
6983
7094
|
filesSection += `
|
|
6984
7095
|
|
|
6985
7096
|
=== ${filePath} ===
|
|
6986
|
-
${content.slice(0,
|
|
6987
|
-
if (content.length >
|
|
7097
|
+
${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
|
|
7098
|
+
if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
|
|
6988
7099
|
... (truncated, ${content.length} chars total)`;
|
|
6989
7100
|
} catch {
|
|
6990
7101
|
filesSection += `
|
|
@@ -7013,7 +7124,7 @@ ${content.slice(0, 3e3)}`;
|
|
|
7013
7124
|
const color = entry.score >= 8 ? import_chalk12.default.green : entry.score >= 6 ? import_chalk12.default.yellow : import_chalk12.default.red;
|
|
7014
7125
|
const impactTag = entry.impactLevel ? import_chalk12.default.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? import_chalk12.default.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? import_chalk12.default.yellow(entry.impactLevel) : import_chalk12.default.green(entry.impactLevel)}`) : "";
|
|
7015
7126
|
const complexityTag = entry.complexityLevel ? import_chalk12.default.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? import_chalk12.default.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? import_chalk12.default.yellow(entry.complexityLevel) : import_chalk12.default.green(entry.complexityLevel)}`) : "";
|
|
7016
|
-
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${
|
|
7127
|
+
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path9.basename(entry.specFile)}`);
|
|
7017
7128
|
}
|
|
7018
7129
|
console.log(import_chalk12.default.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
7019
7130
|
}
|
|
@@ -7066,8 +7177,8 @@ function parseSpecAndTasks(raw) {
|
|
|
7066
7177
|
|
|
7067
7178
|
// git/worktree.ts
|
|
7068
7179
|
var import_child_process4 = require("child_process");
|
|
7069
|
-
var
|
|
7070
|
-
var
|
|
7180
|
+
var path10 = __toESM(require("path"));
|
|
7181
|
+
var fs14 = __toESM(require("fs-extra"));
|
|
7071
7182
|
var import_chalk13 = __toESM(require("chalk"));
|
|
7072
7183
|
var GitWorktreeManager = class {
|
|
7073
7184
|
constructor(baseDir) {
|
|
@@ -7093,12 +7204,12 @@ var GitWorktreeManager = class {
|
|
|
7093
7204
|
async linkDependencies(worktreePath) {
|
|
7094
7205
|
const candidates = ["node_modules", "vendor"];
|
|
7095
7206
|
for (const dir of candidates) {
|
|
7096
|
-
const src =
|
|
7097
|
-
const dest =
|
|
7098
|
-
if (!await
|
|
7099
|
-
if (await
|
|
7207
|
+
const src = path10.join(this.baseDir, dir);
|
|
7208
|
+
const dest = path10.join(worktreePath, dir);
|
|
7209
|
+
if (!await fs14.pathExists(src)) continue;
|
|
7210
|
+
if (await fs14.pathExists(dest)) continue;
|
|
7100
7211
|
try {
|
|
7101
|
-
await
|
|
7212
|
+
await fs14.ensureSymlink(src, dest, "dir");
|
|
7102
7213
|
console.log(import_chalk13.default.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
7103
7214
|
} catch (err) {
|
|
7104
7215
|
console.log(import_chalk13.default.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
@@ -7113,11 +7224,11 @@ var GitWorktreeManager = class {
|
|
|
7113
7224
|
}
|
|
7114
7225
|
const featureName = this.sanitizeFeatureName(idea);
|
|
7115
7226
|
const branchName = `feature/${featureName}`;
|
|
7116
|
-
const repoName =
|
|
7117
|
-
const worktreePath =
|
|
7227
|
+
const repoName = path10.basename(this.baseDir);
|
|
7228
|
+
const worktreePath = path10.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
7118
7229
|
console.log(import_chalk13.default.cyan(`
|
|
7119
7230
|
--- Setting up Git Worktree ---`));
|
|
7120
|
-
if (await
|
|
7231
|
+
if (await fs14.pathExists(worktreePath)) {
|
|
7121
7232
|
console.log(import_chalk13.default.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
7122
7233
|
await this.linkDependencies(worktreePath);
|
|
7123
7234
|
return worktreePath;
|