ai-spec-dev 0.46.0 → 0.56.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 +60 -30
- package/cli/commands/config.ts +129 -1
- package/cli/commands/create.ts +14 -0
- package/cli/commands/fix-history.ts +176 -0
- package/cli/commands/init.ts +36 -1
- package/cli/index.ts +2 -6
- package/cli/pipeline/helpers.ts +6 -0
- package/cli/pipeline/multi-repo.ts +300 -26
- package/cli/pipeline/single-repo.ts +103 -2
- package/cli/utils.ts +23 -0
- package/core/code-generator.ts +63 -14
- package/core/cross-stack-verifier.ts +482 -0
- 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/self-evaluator.ts +44 -7
- package/core/spec-generator.ts +3 -3
- package/core/types-generator.ts +2 -2
- package/dist/cli/index.js +3968 -2353
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +3810 -2195
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +249 -128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +249 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/tests/cross-stack-verifier.test.ts +402 -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/.ai-spec-workspace.json +0 -17
- package/.ai-spec.json +0 -7
- package/cli/commands/model.ts +0 -152
- package/cli/commands/scan.ts +0 -99
- package/cli/commands/workspace.ts +0 -219
package/dist/index.mjs
CHANGED
|
@@ -339,12 +339,12 @@ var PROVIDER_CATALOG = {
|
|
|
339
339
|
},
|
|
340
340
|
deepseek: {
|
|
341
341
|
displayName: "DeepSeek",
|
|
342
|
-
description: "DeepSeek
|
|
342
|
+
description: "DeepSeek V3.2 (chat) / R1 (reasoner) \u2014 alias auto-tracks latest stable",
|
|
343
343
|
models: [
|
|
344
344
|
"deepseek-chat",
|
|
345
|
-
// DeepSeek
|
|
345
|
+
// V3.2 (alias auto-updates as DeepSeek releases new versions)
|
|
346
346
|
"deepseek-reasoner"
|
|
347
|
-
//
|
|
347
|
+
// R1 (reasoning model)
|
|
348
348
|
],
|
|
349
349
|
envKey: "DEEPSEEK_API_KEY",
|
|
350
350
|
baseURL: "https://api.deepseek.com/v1"
|
|
@@ -4167,8 +4167,8 @@ ${currentSpec}`,
|
|
|
4167
4167
|
// core/code-generator.ts
|
|
4168
4168
|
import chalk10 from "chalk";
|
|
4169
4169
|
import { execSync as execSync2, spawnSync } from "child_process";
|
|
4170
|
-
import * as
|
|
4171
|
-
import * as
|
|
4170
|
+
import * as path7 from "path";
|
|
4171
|
+
import * as fs11 from "fs-extra";
|
|
4172
4172
|
|
|
4173
4173
|
// prompts/codegen.prompt.ts
|
|
4174
4174
|
var codeGenSystemPrompt = `You are a Senior Full-Stack Developer implementing features based on provided specifications.
|
|
@@ -4838,32 +4838,32 @@ function validateDsl(raw) {
|
|
|
4838
4838
|
}
|
|
4839
4839
|
return { valid: true, dsl: raw };
|
|
4840
4840
|
}
|
|
4841
|
-
function validateFeature(raw,
|
|
4841
|
+
function validateFeature(raw, path11, errors) {
|
|
4842
4842
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4843
|
-
errors.push({ path:
|
|
4843
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4844
4844
|
return;
|
|
4845
4845
|
}
|
|
4846
4846
|
const f = raw;
|
|
4847
|
-
requireNonEmptyString(f["id"], `${
|
|
4848
|
-
requireNonEmptyString(f["title"], `${
|
|
4849
|
-
requireNonEmptyString(f["description"], `${
|
|
4847
|
+
requireNonEmptyString(f["id"], `${path11}.id`, errors);
|
|
4848
|
+
requireNonEmptyString(f["title"], `${path11}.title`, errors);
|
|
4849
|
+
requireNonEmptyString(f["description"], `${path11}.description`, errors);
|
|
4850
4850
|
}
|
|
4851
|
-
function validateModel(raw,
|
|
4851
|
+
function validateModel(raw, path11, errors) {
|
|
4852
4852
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4853
|
-
errors.push({ path:
|
|
4853
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4854
4854
|
return;
|
|
4855
4855
|
}
|
|
4856
4856
|
const m = raw;
|
|
4857
|
-
requireNonEmptyString(m["name"], `${
|
|
4857
|
+
requireNonEmptyString(m["name"], `${path11}.name`, errors);
|
|
4858
4858
|
if (!Array.isArray(m["fields"])) {
|
|
4859
|
-
errors.push({ path: `${
|
|
4859
|
+
errors.push({ path: `${path11}.fields`, message: `Must be an array, got: ${typeLabel(m["fields"])}` });
|
|
4860
4860
|
} else {
|
|
4861
4861
|
const fields = m["fields"];
|
|
4862
4862
|
if (fields.length > MAX_FIELDS_PER_MODEL) {
|
|
4863
|
-
errors.push({ path: `${
|
|
4863
|
+
errors.push({ path: `${path11}.fields`, message: `Too many fields (${fields.length} > ${MAX_FIELDS_PER_MODEL})` });
|
|
4864
4864
|
}
|
|
4865
4865
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4866
|
-
validateModelField(fields[j2], `${
|
|
4866
|
+
validateModelField(fields[j2], `${path11}.fields[${j2}]`, errors);
|
|
4867
4867
|
}
|
|
4868
4868
|
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
4869
4869
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
@@ -4872,7 +4872,7 @@ function validateModel(raw, path10, errors) {
|
|
|
4872
4872
|
const name = f["name"];
|
|
4873
4873
|
if (seenFieldNames.has(name)) {
|
|
4874
4874
|
errors.push({
|
|
4875
|
-
path: `${
|
|
4875
|
+
path: `${path11}.fields[${j2}].name`,
|
|
4876
4876
|
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
4877
4877
|
});
|
|
4878
4878
|
} else {
|
|
@@ -4883,184 +4883,184 @@ function validateModel(raw, path10, errors) {
|
|
|
4883
4883
|
}
|
|
4884
4884
|
if (m["relations"] !== void 0) {
|
|
4885
4885
|
if (!Array.isArray(m["relations"])) {
|
|
4886
|
-
errors.push({ path: `${
|
|
4886
|
+
errors.push({ path: `${path11}.relations`, message: "Must be an array of strings if present" });
|
|
4887
4887
|
} else {
|
|
4888
4888
|
const rels = m["relations"];
|
|
4889
4889
|
for (let j2 = 0; j2 < rels.length; j2++) {
|
|
4890
4890
|
if (typeof rels[j2] !== "string") {
|
|
4891
|
-
errors.push({ path: `${
|
|
4891
|
+
errors.push({ path: `${path11}.relations[${j2}]`, message: "Must be a string" });
|
|
4892
4892
|
}
|
|
4893
4893
|
}
|
|
4894
4894
|
}
|
|
4895
4895
|
}
|
|
4896
4896
|
}
|
|
4897
|
-
function validateModelField(raw,
|
|
4897
|
+
function validateModelField(raw, path11, errors) {
|
|
4898
4898
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4899
|
-
errors.push({ path:
|
|
4899
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4900
4900
|
return;
|
|
4901
4901
|
}
|
|
4902
4902
|
const f = raw;
|
|
4903
|
-
requireNonEmptyString(f["name"], `${
|
|
4904
|
-
requireNonEmptyString(f["type"], `${
|
|
4903
|
+
requireNonEmptyString(f["name"], `${path11}.name`, errors);
|
|
4904
|
+
requireNonEmptyString(f["type"], `${path11}.type`, errors);
|
|
4905
4905
|
if (typeof f["required"] !== "boolean") {
|
|
4906
|
-
errors.push({ path: `${
|
|
4906
|
+
errors.push({ path: `${path11}.required`, message: `Must be boolean, got: ${typeLabel(f["required"])}` });
|
|
4907
4907
|
}
|
|
4908
4908
|
}
|
|
4909
|
-
function validateEndpoint(raw,
|
|
4909
|
+
function validateEndpoint(raw, path11, errors) {
|
|
4910
4910
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4911
|
-
errors.push({ path:
|
|
4911
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4912
4912
|
return;
|
|
4913
4913
|
}
|
|
4914
4914
|
const e = raw;
|
|
4915
|
-
requireNonEmptyString(e["id"], `${
|
|
4916
|
-
requireNonEmptyString(e["description"], `${
|
|
4915
|
+
requireNonEmptyString(e["id"], `${path11}.id`, errors);
|
|
4916
|
+
requireNonEmptyString(e["description"], `${path11}.description`, errors);
|
|
4917
4917
|
if (!VALID_METHODS.includes(e["method"])) {
|
|
4918
4918
|
errors.push({
|
|
4919
|
-
path: `${
|
|
4919
|
+
path: `${path11}.method`,
|
|
4920
4920
|
message: `Must be one of ${VALID_METHODS.join("|")}, got: ${JSON.stringify(e["method"])}`
|
|
4921
4921
|
});
|
|
4922
4922
|
}
|
|
4923
4923
|
if (typeof e["path"] !== "string" || !e["path"].startsWith("/")) {
|
|
4924
4924
|
errors.push({
|
|
4925
|
-
path: `${
|
|
4925
|
+
path: `${path11}.path`,
|
|
4926
4926
|
message: `Must be a string starting with "/", got: ${JSON.stringify(e["path"])}`
|
|
4927
4927
|
});
|
|
4928
4928
|
}
|
|
4929
4929
|
if (typeof e["auth"] !== "boolean") {
|
|
4930
|
-
errors.push({ path: `${
|
|
4930
|
+
errors.push({ path: `${path11}.auth`, message: `Must be boolean, got: ${typeLabel(e["auth"])}` });
|
|
4931
4931
|
}
|
|
4932
4932
|
if (typeof e["successStatus"] !== "number" || e["successStatus"] < 100 || e["successStatus"] > 599) {
|
|
4933
4933
|
errors.push({
|
|
4934
|
-
path: `${
|
|
4934
|
+
path: `${path11}.successStatus`,
|
|
4935
4935
|
message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["successStatus"])}`
|
|
4936
4936
|
});
|
|
4937
4937
|
}
|
|
4938
|
-
requireNonEmptyString(e["successDescription"], `${
|
|
4938
|
+
requireNonEmptyString(e["successDescription"], `${path11}.successDescription`, errors);
|
|
4939
4939
|
if (e["request"] !== void 0) {
|
|
4940
|
-
validateRequestSchema(e["request"], `${
|
|
4940
|
+
validateRequestSchema(e["request"], `${path11}.request`, errors);
|
|
4941
4941
|
}
|
|
4942
4942
|
if (e["errors"] !== void 0) {
|
|
4943
4943
|
if (!Array.isArray(e["errors"])) {
|
|
4944
|
-
errors.push({ path: `${
|
|
4944
|
+
errors.push({ path: `${path11}.errors`, message: "Must be an array if present" });
|
|
4945
4945
|
} else {
|
|
4946
4946
|
const errs = e["errors"];
|
|
4947
4947
|
if (errs.length > MAX_ERRORS_PER_ENDPOINT) {
|
|
4948
|
-
errors.push({ path: `${
|
|
4948
|
+
errors.push({ path: `${path11}.errors`, message: `Too many error entries (${errs.length} > ${MAX_ERRORS_PER_ENDPOINT})` });
|
|
4949
4949
|
}
|
|
4950
4950
|
for (let j2 = 0; j2 < Math.min(errs.length, MAX_ERRORS_PER_ENDPOINT); j2++) {
|
|
4951
|
-
validateResponseError(errs[j2], `${
|
|
4951
|
+
validateResponseError(errs[j2], `${path11}.errors[${j2}]`, errors);
|
|
4952
4952
|
}
|
|
4953
4953
|
}
|
|
4954
4954
|
}
|
|
4955
4955
|
}
|
|
4956
|
-
function validateRequestSchema(raw,
|
|
4956
|
+
function validateRequestSchema(raw, path11, errors) {
|
|
4957
4957
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4958
|
-
errors.push({ path:
|
|
4958
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4959
4959
|
return;
|
|
4960
4960
|
}
|
|
4961
4961
|
const r = raw;
|
|
4962
4962
|
for (const key of ["body", "query", "params"]) {
|
|
4963
4963
|
if (r[key] !== void 0) {
|
|
4964
|
-
validateFieldMap(r[key], `${
|
|
4964
|
+
validateFieldMap(r[key], `${path11}.${key}`, errors);
|
|
4965
4965
|
}
|
|
4966
4966
|
}
|
|
4967
4967
|
}
|
|
4968
|
-
function validateFieldMap(raw,
|
|
4968
|
+
function validateFieldMap(raw, path11, errors) {
|
|
4969
4969
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4970
|
-
errors.push({ path:
|
|
4970
|
+
errors.push({ path: path11, message: `Must be a flat object (FieldMap), got: ${typeLabel(raw)}` });
|
|
4971
4971
|
return;
|
|
4972
4972
|
}
|
|
4973
4973
|
const map = raw;
|
|
4974
4974
|
for (const [k2, v2] of Object.entries(map)) {
|
|
4975
4975
|
if (typeof v2 !== "string") {
|
|
4976
|
-
errors.push({ path: `${
|
|
4976
|
+
errors.push({ path: `${path11}.${k2}`, message: `Value must be a type-description string, got: ${typeLabel(v2)}` });
|
|
4977
4977
|
}
|
|
4978
4978
|
}
|
|
4979
4979
|
}
|
|
4980
|
-
function validateResponseError(raw,
|
|
4980
|
+
function validateResponseError(raw, path11, errors) {
|
|
4981
4981
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4982
|
-
errors.push({ path:
|
|
4982
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4983
4983
|
return;
|
|
4984
4984
|
}
|
|
4985
4985
|
const e = raw;
|
|
4986
4986
|
if (typeof e["status"] !== "number" || e["status"] < 100 || e["status"] > 599) {
|
|
4987
|
-
errors.push({ path: `${
|
|
4987
|
+
errors.push({ path: `${path11}.status`, message: `Must be an HTTP status code (100-599), got: ${JSON.stringify(e["status"])}` });
|
|
4988
4988
|
}
|
|
4989
|
-
requireNonEmptyString(e["code"], `${
|
|
4990
|
-
requireNonEmptyString(e["description"], `${
|
|
4989
|
+
requireNonEmptyString(e["code"], `${path11}.code`, errors);
|
|
4990
|
+
requireNonEmptyString(e["description"], `${path11}.description`, errors);
|
|
4991
4991
|
}
|
|
4992
|
-
function validateBehavior(raw,
|
|
4992
|
+
function validateBehavior(raw, path11, errors) {
|
|
4993
4993
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
4994
|
-
errors.push({ path:
|
|
4994
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
4995
4995
|
return;
|
|
4996
4996
|
}
|
|
4997
4997
|
const b = raw;
|
|
4998
|
-
requireNonEmptyString(b["id"], `${
|
|
4999
|
-
requireNonEmptyString(b["description"], `${
|
|
4998
|
+
requireNonEmptyString(b["id"], `${path11}.id`, errors);
|
|
4999
|
+
requireNonEmptyString(b["description"], `${path11}.description`, errors);
|
|
5000
5000
|
if (b["constraints"] !== void 0) {
|
|
5001
5001
|
if (!Array.isArray(b["constraints"])) {
|
|
5002
|
-
errors.push({ path: `${
|
|
5002
|
+
errors.push({ path: `${path11}.constraints`, message: "Must be an array of strings if present" });
|
|
5003
5003
|
} else {
|
|
5004
5004
|
const cs2 = b["constraints"];
|
|
5005
5005
|
for (let j2 = 0; j2 < cs2.length; j2++) {
|
|
5006
5006
|
if (typeof cs2[j2] !== "string") {
|
|
5007
|
-
errors.push({ path: `${
|
|
5007
|
+
errors.push({ path: `${path11}.constraints[${j2}]`, message: "Must be a string" });
|
|
5008
5008
|
}
|
|
5009
5009
|
}
|
|
5010
5010
|
}
|
|
5011
5011
|
}
|
|
5012
5012
|
}
|
|
5013
|
-
function validateComponent(raw,
|
|
5013
|
+
function validateComponent(raw, path11, errors) {
|
|
5014
5014
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5015
|
-
errors.push({ path:
|
|
5015
|
+
errors.push({ path: path11, message: `Must be an object, got: ${typeLabel(raw)}` });
|
|
5016
5016
|
return;
|
|
5017
5017
|
}
|
|
5018
5018
|
const c = raw;
|
|
5019
|
-
requireNonEmptyString(c["id"], `${
|
|
5020
|
-
requireNonEmptyString(c["name"], `${
|
|
5021
|
-
requireNonEmptyString(c["description"], `${
|
|
5019
|
+
requireNonEmptyString(c["id"], `${path11}.id`, errors);
|
|
5020
|
+
requireNonEmptyString(c["name"], `${path11}.name`, errors);
|
|
5021
|
+
requireNonEmptyString(c["description"], `${path11}.description`, errors);
|
|
5022
5022
|
if (c["props"] !== void 0) {
|
|
5023
5023
|
if (!Array.isArray(c["props"])) {
|
|
5024
|
-
errors.push({ path: `${
|
|
5024
|
+
errors.push({ path: `${path11}.props`, message: "Must be an array if present" });
|
|
5025
5025
|
} else {
|
|
5026
5026
|
const props = c["props"];
|
|
5027
5027
|
for (let j2 = 0; j2 < props.length; j2++) {
|
|
5028
5028
|
const p = props[j2];
|
|
5029
5029
|
if (typeof p !== "object" || p === null) {
|
|
5030
|
-
errors.push({ path: `${
|
|
5030
|
+
errors.push({ path: `${path11}.props[${j2}]`, message: "Must be an object" });
|
|
5031
5031
|
continue;
|
|
5032
5032
|
}
|
|
5033
|
-
requireNonEmptyString(p["name"], `${
|
|
5034
|
-
requireNonEmptyString(p["type"], `${
|
|
5033
|
+
requireNonEmptyString(p["name"], `${path11}.props[${j2}].name`, errors);
|
|
5034
|
+
requireNonEmptyString(p["type"], `${path11}.props[${j2}].type`, errors);
|
|
5035
5035
|
if (typeof p["required"] !== "boolean") {
|
|
5036
|
-
errors.push({ path: `${
|
|
5036
|
+
errors.push({ path: `${path11}.props[${j2}].required`, message: "Must be boolean" });
|
|
5037
5037
|
}
|
|
5038
5038
|
}
|
|
5039
5039
|
}
|
|
5040
5040
|
}
|
|
5041
5041
|
if (c["events"] !== void 0) {
|
|
5042
5042
|
if (!Array.isArray(c["events"])) {
|
|
5043
|
-
errors.push({ path: `${
|
|
5043
|
+
errors.push({ path: `${path11}.events`, message: "Must be an array if present" });
|
|
5044
5044
|
} else {
|
|
5045
5045
|
const events = c["events"];
|
|
5046
5046
|
for (let j2 = 0; j2 < events.length; j2++) {
|
|
5047
5047
|
const e = events[j2];
|
|
5048
5048
|
if (typeof e !== "object" || e === null) {
|
|
5049
|
-
errors.push({ path: `${
|
|
5049
|
+
errors.push({ path: `${path11}.events[${j2}]`, message: "Must be an object" });
|
|
5050
5050
|
continue;
|
|
5051
5051
|
}
|
|
5052
|
-
requireNonEmptyString(e["name"], `${
|
|
5052
|
+
requireNonEmptyString(e["name"], `${path11}.events[${j2}].name`, errors);
|
|
5053
5053
|
}
|
|
5054
5054
|
}
|
|
5055
5055
|
}
|
|
5056
5056
|
if (c["state"] !== void 0) {
|
|
5057
5057
|
if (typeof c["state"] !== "object" || Array.isArray(c["state"]) || c["state"] === null) {
|
|
5058
|
-
errors.push({ path: `${
|
|
5058
|
+
errors.push({ path: `${path11}.state`, message: "Must be a flat object (Record<string, string>) if present" });
|
|
5059
5059
|
}
|
|
5060
5060
|
}
|
|
5061
5061
|
if (c["apiCalls"] !== void 0) {
|
|
5062
5062
|
if (!Array.isArray(c["apiCalls"])) {
|
|
5063
|
-
errors.push({ path: `${
|
|
5063
|
+
errors.push({ path: `${path11}.apiCalls`, message: "Must be an array of strings if present" });
|
|
5064
5064
|
}
|
|
5065
5065
|
}
|
|
5066
5066
|
}
|
|
@@ -5122,10 +5122,10 @@ function crossReferenceChecks(obj, errors) {
|
|
|
5122
5122
|
}
|
|
5123
5123
|
}
|
|
5124
5124
|
}
|
|
5125
|
-
function requireNonEmptyString(v2,
|
|
5125
|
+
function requireNonEmptyString(v2, path11, errors) {
|
|
5126
5126
|
if (typeof v2 !== "string" || v2.trim().length === 0) {
|
|
5127
5127
|
errors.push({
|
|
5128
|
-
path:
|
|
5128
|
+
path: path11,
|
|
5129
5129
|
message: `Must be a non-empty string, got: ${typeLabel(v2)}`
|
|
5130
5130
|
});
|
|
5131
5131
|
}
|
|
@@ -6014,6 +6014,101 @@ function printTaskProgress(completed, total, task, mode) {
|
|
|
6014
6014
|
}
|
|
6015
6015
|
}
|
|
6016
6016
|
|
|
6017
|
+
// core/fix-history.ts
|
|
6018
|
+
import * as fs10 from "fs-extra";
|
|
6019
|
+
import * as path6 from "path";
|
|
6020
|
+
var FIX_HISTORY_FILE = ".ai-spec-fix-history.json";
|
|
6021
|
+
var FIX_HISTORY_VERSION = "1.0";
|
|
6022
|
+
async function loadFixHistory(repoRoot) {
|
|
6023
|
+
const filePath = path6.join(repoRoot, FIX_HISTORY_FILE);
|
|
6024
|
+
if (!await fs10.pathExists(filePath)) {
|
|
6025
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6026
|
+
}
|
|
6027
|
+
try {
|
|
6028
|
+
const data = await fs10.readJson(filePath);
|
|
6029
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.entries)) {
|
|
6030
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6031
|
+
}
|
|
6032
|
+
return {
|
|
6033
|
+
version: typeof data.version === "string" ? data.version : FIX_HISTORY_VERSION,
|
|
6034
|
+
entries: data.entries
|
|
6035
|
+
};
|
|
6036
|
+
} catch {
|
|
6037
|
+
return { version: FIX_HISTORY_VERSION, entries: [] };
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
function aggregateFixPatterns(history) {
|
|
6041
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6042
|
+
for (const entry of history.entries) {
|
|
6043
|
+
if (!byKey.has(entry.patternKey)) byKey.set(entry.patternKey, []);
|
|
6044
|
+
byKey.get(entry.patternKey).push(entry);
|
|
6045
|
+
}
|
|
6046
|
+
const aggregates = [];
|
|
6047
|
+
for (const [patternKey, entries] of byKey) {
|
|
6048
|
+
entries.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
6049
|
+
const first = entries[0];
|
|
6050
|
+
const last = entries[entries.length - 1];
|
|
6051
|
+
const uniqueRunIds = new Set(entries.map((e) => e.runId)).size;
|
|
6052
|
+
aggregates.push({
|
|
6053
|
+
patternKey,
|
|
6054
|
+
count: entries.length,
|
|
6055
|
+
firstSeen: first.ts,
|
|
6056
|
+
lastSeen: last.ts,
|
|
6057
|
+
uniqueRunIds,
|
|
6058
|
+
source: last.brokenImport.source,
|
|
6059
|
+
names: last.brokenImport.names,
|
|
6060
|
+
reason: last.brokenImport.reason,
|
|
6061
|
+
fix: {
|
|
6062
|
+
kind: last.fix.kind,
|
|
6063
|
+
target: last.fix.target,
|
|
6064
|
+
stage: last.fix.stage
|
|
6065
|
+
}
|
|
6066
|
+
});
|
|
6067
|
+
}
|
|
6068
|
+
aggregates.sort((a, b) => {
|
|
6069
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
6070
|
+
return b.lastSeen.localeCompare(a.lastSeen);
|
|
6071
|
+
});
|
|
6072
|
+
return aggregates;
|
|
6073
|
+
}
|
|
6074
|
+
function buildHallucinationAvoidanceSection(history, opts = {}) {
|
|
6075
|
+
const minCount = opts.minCount ?? 1;
|
|
6076
|
+
const maxItems = opts.maxItems ?? 10;
|
|
6077
|
+
const patterns = aggregateFixPatterns(history).filter((p) => p.count >= minCount);
|
|
6078
|
+
if (patterns.length === 0) return null;
|
|
6079
|
+
const top = patterns.slice(0, maxItems);
|
|
6080
|
+
const lines = [
|
|
6081
|
+
"=== Prior Hallucinations in This Project (DO NOT REPEAT) ===",
|
|
6082
|
+
"",
|
|
6083
|
+
"The following imports were previously hallucinated by AI codegen in this",
|
|
6084
|
+
"project and had to be auto-fixed. When generating new files, actively avoid",
|
|
6085
|
+
"these exact imports \u2014 they were wrong in the past and will be wrong again.",
|
|
6086
|
+
""
|
|
6087
|
+
];
|
|
6088
|
+
for (const p of top) {
|
|
6089
|
+
const namesLabel = p.names.length > 0 ? `{ ${p.names.join(", ")} }` : "(no names)";
|
|
6090
|
+
const reasonLabel = p.reason === "file_not_found" ? "file did not exist" : "named export did not exist";
|
|
6091
|
+
const countLabel = p.count === 1 ? "1x" : `${p.count}x`;
|
|
6092
|
+
const dateLabel = p.lastSeen.slice(0, 10);
|
|
6093
|
+
lines.push(`\u274C Do NOT: import ${namesLabel} from '${p.source}'`);
|
|
6094
|
+
lines.push(` Reason: ${reasonLabel} (seen ${countLabel}, last ${dateLabel})`);
|
|
6095
|
+
if (p.fix.kind === "create_file") {
|
|
6096
|
+
lines.push(` Previously fixed by creating: ${p.fix.target}`);
|
|
6097
|
+
} else if (p.fix.kind === "rewrite_import") {
|
|
6098
|
+
lines.push(` Previously fixed by rewriting the import path`);
|
|
6099
|
+
} else {
|
|
6100
|
+
lines.push(` Previously fixed by appending to: ${p.fix.target}`);
|
|
6101
|
+
}
|
|
6102
|
+
lines.push("");
|
|
6103
|
+
}
|
|
6104
|
+
if (patterns.length > maxItems) {
|
|
6105
|
+
lines.push(`(${patterns.length - maxItems} more pattern(s) hidden \u2014 run \`ai-spec fix-history\` to see all)`);
|
|
6106
|
+
lines.push("");
|
|
6107
|
+
}
|
|
6108
|
+
lines.push("=== End of Prior Hallucinations ===");
|
|
6109
|
+
return lines.join("\n");
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6017
6112
|
// core/code-generator.ts
|
|
6018
6113
|
var CodeGenerator = class {
|
|
6019
6114
|
constructor(provider, mode = "claude-code") {
|
|
@@ -6078,8 +6173,8 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
6078
6173
|
Files: ${t.filesToTouch.join(", ")}
|
|
6079
6174
|
Criteria: ${t.acceptanceCriteria.join("; ")}`).join("\n")}` : "";
|
|
6080
6175
|
const promptContent = `Please read the spec file at ${specFilePath} and implement all the requirements. Create or modify files as necessary.${taskSection}`;
|
|
6081
|
-
const promptFile =
|
|
6082
|
-
await
|
|
6176
|
+
const promptFile = path7.join(workingDir, ".claude-prompt.txt");
|
|
6177
|
+
await fs11.writeFile(promptFile, promptContent, "utf-8");
|
|
6083
6178
|
if (options.auto) {
|
|
6084
6179
|
console.log(chalk10.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6085
6180
|
console.log(chalk10.gray(` Spec: ${specFilePath}`));
|
|
@@ -6172,7 +6267,7 @@ Implement ONLY this task. Do not implement other tasks.`;
|
|
|
6172
6267
|
if (options.repoType && options.repoType !== "node-express" && options.repoType !== "node-koa" && options.repoType !== "unknown") {
|
|
6173
6268
|
console.log(chalk10.gray(` Language: ${options.repoType} (using language-specific codegen prompt)`));
|
|
6174
6269
|
}
|
|
6175
|
-
const spec = await
|
|
6270
|
+
const spec = await fs11.readFile(specFilePath, "utf-8");
|
|
6176
6271
|
let constitutionSection = context?.constitution ? `
|
|
6177
6272
|
=== Project Constitution (MUST follow) ===
|
|
6178
6273
|
${context.constitution}
|
|
@@ -6199,7 +6294,24 @@ ${buildFrontendContextSection(fctx)}
|
|
|
6199
6294
|
`;
|
|
6200
6295
|
console.log(chalk10.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
|
|
6201
6296
|
}
|
|
6202
|
-
|
|
6297
|
+
let fixHistorySection = "";
|
|
6298
|
+
if (options.injectFixHistory !== false) {
|
|
6299
|
+
try {
|
|
6300
|
+
const history = await loadFixHistory(workingDir);
|
|
6301
|
+
const section = buildHallucinationAvoidanceSection(history, {
|
|
6302
|
+
maxItems: options.fixHistoryInjectMax ?? 10
|
|
6303
|
+
});
|
|
6304
|
+
if (section) {
|
|
6305
|
+
fixHistorySection = `
|
|
6306
|
+
${section}
|
|
6307
|
+
`;
|
|
6308
|
+
const patternCount = (section.match(/❌ Do NOT/g) ?? []).length;
|
|
6309
|
+
console.log(chalk10.cyan(` \u2713 Injected ${patternCount} prior hallucination pattern(s) from fix-history`));
|
|
6310
|
+
}
|
|
6311
|
+
} catch {
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection + fixHistorySection;
|
|
6203
6315
|
const estimatedTokenCount = estimateTokens(allContextText);
|
|
6204
6316
|
const budget = getDefaultBudget(this.provider.providerName);
|
|
6205
6317
|
if (estimatedTokenCount > budget * 0.7) {
|
|
@@ -6218,7 +6330,7 @@ ${buildFrontendContextSection(fctx)}
|
|
|
6218
6330
|
}
|
|
6219
6331
|
const tasks = await loadTasksForSpec(specFilePath);
|
|
6220
6332
|
if (tasks && tasks.length > 0) {
|
|
6221
|
-
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6333
|
+
return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection + fixHistorySection, frontendSection, sharedConfigSection, options, systemPrompt, context);
|
|
6222
6334
|
}
|
|
6223
6335
|
console.log(chalk10.gray(" [1/2] Planning implementation files..."));
|
|
6224
6336
|
const planPrompt = `Based on the feature spec and project context below, list ALL files that need to be created or modified.
|
|
@@ -6229,7 +6341,7 @@ IMPORTANT: Check the "Frontend Project Context" section below. Extend existing h
|
|
|
6229
6341
|
|
|
6230
6342
|
=== Feature Spec ===
|
|
6231
6343
|
${spec}
|
|
6232
|
-
${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
|
|
6344
|
+
${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}${fixHistorySection}
|
|
6233
6345
|
=== Project Context ===
|
|
6234
6346
|
${contextSummary}
|
|
6235
6347
|
|
|
@@ -6256,7 +6368,7 @@ Output ONLY a valid JSON array:
|
|
|
6256
6368
|
const icon = item.action === "create" ? chalk10.green("+") : chalk10.yellow("~");
|
|
6257
6369
|
console.log(` ${icon} ${item.file}: ${chalk10.gray(item.description)}`);
|
|
6258
6370
|
});
|
|
6259
|
-
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
|
|
6371
|
+
const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection + fixHistorySection, systemPrompt);
|
|
6260
6372
|
return files;
|
|
6261
6373
|
}
|
|
6262
6374
|
async runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection, frontendSection = "", sharedConfigSection = "", options = {}, systemPrompt = getCodeGenSystemPrompt(), context) {
|
|
@@ -6316,7 +6428,7 @@ Output ONLY a valid JSON array:
|
|
|
6316
6428
|
}
|
|
6317
6429
|
const filePlan = await Promise.all(
|
|
6318
6430
|
task.filesToTouch.filter((f) => !sharedConfigPaths.has(f)).map(async (f) => {
|
|
6319
|
-
const exists = await
|
|
6431
|
+
const exists = await fs11.pathExists(path7.join(workingDir, f));
|
|
6320
6432
|
return {
|
|
6321
6433
|
file: f,
|
|
6322
6434
|
action: exists ? "modify" : "create",
|
|
@@ -6356,7 +6468,7 @@ ${taskContext}`,
|
|
|
6356
6468
|
const isViewFile = /src[\\/](views?|pages?)[\\/]/i.test(writtenFile);
|
|
6357
6469
|
if (isCodeFile || isViewFile) {
|
|
6358
6470
|
try {
|
|
6359
|
-
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await
|
|
6471
|
+
const content = isViewFile ? `// view component \u2014 use this exact path for router imports` : await fs11.readFile(path7.join(workingDir, writtenFile), "utf-8");
|
|
6360
6472
|
generatedFileCache.set(writtenFile, content);
|
|
6361
6473
|
} catch {
|
|
6362
6474
|
}
|
|
@@ -6366,19 +6478,28 @@ ${taskContext}`,
|
|
|
6366
6478
|
};
|
|
6367
6479
|
const taskBatches = topoSortLayerTasks(layerTasks);
|
|
6368
6480
|
const layerResults = [];
|
|
6481
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? 3);
|
|
6369
6482
|
for (const batch of taskBatches) {
|
|
6370
6483
|
const batchIsParallel = batch.length > 1;
|
|
6371
|
-
const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
|
|
6372
|
-
const settled = await Promise.allSettled(batchResultPromises);
|
|
6373
6484
|
const batchResults = [];
|
|
6374
|
-
for (let
|
|
6375
|
-
const
|
|
6376
|
-
if (
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6485
|
+
for (let chunkStart = 0; chunkStart < batch.length; chunkStart += maxConcurrency) {
|
|
6486
|
+
const chunk = batch.slice(chunkStart, chunkStart + maxConcurrency);
|
|
6487
|
+
if (batchIsParallel && batch.length > maxConcurrency) {
|
|
6488
|
+
const chunkIdx = Math.floor(chunkStart / maxConcurrency) + 1;
|
|
6489
|
+
const totalChunks = Math.ceil(batch.length / maxConcurrency);
|
|
6490
|
+
console.log(chalk10.gray(` \u21B3 chunk ${chunkIdx}/${totalChunks} (${chunk.length} tasks, concurrency cap: ${maxConcurrency})`));
|
|
6491
|
+
}
|
|
6492
|
+
const chunkResultPromises = chunk.map((task) => executeTask(task, batchIsParallel));
|
|
6493
|
+
const settled = await Promise.allSettled(chunkResultPromises);
|
|
6494
|
+
for (let i = 0; i < settled.length; i++) {
|
|
6495
|
+
const outcome = settled[i];
|
|
6496
|
+
if (outcome.status === "fulfilled") {
|
|
6497
|
+
batchResults.push(outcome.value);
|
|
6498
|
+
} else {
|
|
6499
|
+
const task = chunk[i];
|
|
6500
|
+
console.log(chalk10.yellow(` \u26A0 ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
|
|
6501
|
+
batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
|
|
6502
|
+
}
|
|
6382
6503
|
}
|
|
6383
6504
|
}
|
|
6384
6505
|
layerResults.push(...batchResults);
|
|
@@ -6408,7 +6529,7 @@ ${taskContext}`,
|
|
|
6408
6529
|
const allCreatedInLayer = layerResults.flatMap((r) => r.createdFiles);
|
|
6409
6530
|
for (const sharedFile of context.sharedConfigFiles) {
|
|
6410
6531
|
if (processedSharedConfigs.has(sharedFile.path)) continue;
|
|
6411
|
-
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) =>
|
|
6532
|
+
const newModuleNames = allCreatedInLayer.filter((f) => f !== sharedFile.path).map((f) => path7.basename(f).replace(/\.[jt]sx?$/, ""));
|
|
6412
6533
|
if (newModuleNames.length === 0 && sharedFile.category !== "route-index" && sharedFile.category !== "store-index") continue;
|
|
6413
6534
|
let purpose = `Register/update ${sharedFile.category} entries for the new feature`;
|
|
6414
6535
|
if ((sharedFile.category === "route-index" || sharedFile.category === "store-index") && newModuleNames.length > 0) {
|
|
@@ -6448,10 +6569,10 @@ Updating shared registration after layer [${layer}] completed. New modules: ${ne
|
|
|
6448
6569
|
let successCount = 0;
|
|
6449
6570
|
const writtenFiles = [];
|
|
6450
6571
|
for (const item of filePlan) {
|
|
6451
|
-
const fullPath =
|
|
6572
|
+
const fullPath = path7.join(workingDir, item.file);
|
|
6452
6573
|
let existingContent = "";
|
|
6453
|
-
if (await
|
|
6454
|
-
existingContent = await
|
|
6574
|
+
if (await fs11.pathExists(fullPath)) {
|
|
6575
|
+
existingContent = await fs11.readFile(fullPath, "utf-8");
|
|
6455
6576
|
}
|
|
6456
6577
|
const codePrompt = `Implement this file.
|
|
6457
6578
|
|
|
@@ -6468,8 +6589,8 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6468
6589
|
const raw = await this.provider.generate(codePrompt, systemPrompt);
|
|
6469
6590
|
const fileContent = stripCodeFences(raw);
|
|
6470
6591
|
await getActiveSnapshot()?.snapshotFile(fullPath);
|
|
6471
|
-
await
|
|
6472
|
-
await
|
|
6592
|
+
await fs11.ensureDir(path7.dirname(fullPath));
|
|
6593
|
+
await fs11.writeFile(fullPath, fileContent, "utf-8");
|
|
6473
6594
|
getActiveLogger()?.fileWritten(item.file);
|
|
6474
6595
|
fileSpinner.succeed(`${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)}`);
|
|
6475
6596
|
successCount++;
|
|
@@ -6490,7 +6611,7 @@ ${existingContent || "Output only the complete file content."}`;
|
|
|
6490
6611
|
// ── Mode: plan ─────────────────────────────────────────────────────────────
|
|
6491
6612
|
async runPlanMode(specFilePath) {
|
|
6492
6613
|
console.log(chalk10.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"));
|
|
6493
|
-
const spec = await
|
|
6614
|
+
const spec = await fs11.readFile(specFilePath, "utf-8");
|
|
6494
6615
|
const plan = await this.provider.generate(
|
|
6495
6616
|
`Create a detailed, step-by-step implementation plan for the following feature spec.
|
|
6496
6617
|
Be specific about:
|
|
@@ -6509,13 +6630,13 @@ ${spec}`,
|
|
|
6509
6630
|
// core/reviewer.ts
|
|
6510
6631
|
import chalk12 from "chalk";
|
|
6511
6632
|
import { execSync as execSync3 } from "child_process";
|
|
6512
|
-
import * as
|
|
6513
|
-
import * as
|
|
6633
|
+
import * as path9 from "path";
|
|
6634
|
+
import * as fs13 from "fs-extra";
|
|
6514
6635
|
|
|
6515
6636
|
// core/constitution-generator.ts
|
|
6516
6637
|
import chalk11 from "chalk";
|
|
6517
|
-
import * as
|
|
6518
|
-
import * as
|
|
6638
|
+
import * as fs12 from "fs-extra";
|
|
6639
|
+
import * as path8 from "path";
|
|
6519
6640
|
|
|
6520
6641
|
// prompts/constitution.prompt.ts
|
|
6521
6642
|
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.
|
|
@@ -6595,8 +6716,8 @@ var ConstitutionGenerator = class {
|
|
|
6595
6716
|
return this.provider.generate(prompt, constitutionSystemPrompt);
|
|
6596
6717
|
}
|
|
6597
6718
|
async saveConstitution(projectRoot, content) {
|
|
6598
|
-
const filePath =
|
|
6599
|
-
await
|
|
6719
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6720
|
+
await fs12.writeFile(filePath, content, "utf-8");
|
|
6600
6721
|
return filePath;
|
|
6601
6722
|
}
|
|
6602
6723
|
};
|
|
@@ -6654,9 +6775,9 @@ ${sections.join("\n")}
|
|
|
6654
6775
|
return parts.join("\n");
|
|
6655
6776
|
}
|
|
6656
6777
|
async function loadConstitution(projectRoot) {
|
|
6657
|
-
const filePath =
|
|
6658
|
-
if (await
|
|
6659
|
-
return
|
|
6778
|
+
const filePath = path8.join(projectRoot, CONSTITUTION_FILE);
|
|
6779
|
+
if (await fs12.pathExists(filePath)) {
|
|
6780
|
+
return fs12.readFile(filePath, "utf-8");
|
|
6660
6781
|
}
|
|
6661
6782
|
return void 0;
|
|
6662
6783
|
}
|
|
@@ -6672,10 +6793,10 @@ function printConstitutionHint(exists) {
|
|
|
6672
6793
|
|
|
6673
6794
|
// core/reviewer.ts
|
|
6674
6795
|
async function loadAccumulatedLessons(projectRoot) {
|
|
6675
|
-
const constitutionPath =
|
|
6796
|
+
const constitutionPath = path9.join(projectRoot, CONSTITUTION_FILE);
|
|
6676
6797
|
let content;
|
|
6677
6798
|
try {
|
|
6678
|
-
content = await
|
|
6799
|
+
content = await fs13.readFile(constitutionPath, "utf-8");
|
|
6679
6800
|
} catch {
|
|
6680
6801
|
return null;
|
|
6681
6802
|
}
|
|
@@ -6688,21 +6809,21 @@ async function loadAccumulatedLessons(projectRoot) {
|
|
|
6688
6809
|
}
|
|
6689
6810
|
var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
|
|
6690
6811
|
async function loadReviewHistory(projectRoot) {
|
|
6691
|
-
const historyPath =
|
|
6812
|
+
const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6692
6813
|
try {
|
|
6693
|
-
if (await
|
|
6694
|
-
return await
|
|
6814
|
+
if (await fs13.pathExists(historyPath)) {
|
|
6815
|
+
return await fs13.readJson(historyPath);
|
|
6695
6816
|
}
|
|
6696
6817
|
} catch {
|
|
6697
6818
|
}
|
|
6698
6819
|
return [];
|
|
6699
6820
|
}
|
|
6700
6821
|
async function appendReviewHistory(projectRoot, entry) {
|
|
6701
|
-
const historyPath =
|
|
6822
|
+
const historyPath = path9.join(projectRoot, REVIEW_HISTORY_FILE);
|
|
6702
6823
|
const existing = await loadReviewHistory(projectRoot);
|
|
6703
6824
|
const updated = [...existing, entry].slice(-20);
|
|
6704
6825
|
try {
|
|
6705
|
-
await
|
|
6826
|
+
await fs13.writeJson(historyPath, updated, { spaces: 2 });
|
|
6706
6827
|
} catch {
|
|
6707
6828
|
}
|
|
6708
6829
|
}
|
|
@@ -6736,7 +6857,7 @@ function buildHistoryContext(history) {
|
|
|
6736
6857
|
const lines = ["\n=== \u5386\u53F2\u5BA1\u67E5\u95EE\u9898 (Past Review Issues \u2014 check if any recur) ==="];
|
|
6737
6858
|
for (const entry of recent) {
|
|
6738
6859
|
lines.push(`
|
|
6739
|
-
[${entry.date}] ${
|
|
6860
|
+
[${entry.date}] ${path9.basename(entry.specFile)} \u2014 Score: ${entry.score}/10`);
|
|
6740
6861
|
entry.topIssues.forEach((issue) => lines.push(` \xB7 ${issue}`));
|
|
6741
6862
|
}
|
|
6742
6863
|
return lines.join("\n") + "\n";
|
|
@@ -6856,7 +6977,7 @@ ${sep}
|
|
|
6856
6977
|
if (score > 0 && specFile) {
|
|
6857
6978
|
await appendReviewHistory(this.projectRoot, {
|
|
6858
6979
|
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
6859
|
-
specFile:
|
|
6980
|
+
specFile: path9.relative(this.projectRoot, specFile),
|
|
6860
6981
|
score,
|
|
6861
6982
|
...complianceScore > 0 ? { complianceScore } : {},
|
|
6862
6983
|
topIssues,
|
|
@@ -6901,9 +7022,9 @@ ${sep}
|
|
|
6901
7022
|
);
|
|
6902
7023
|
let filesSection = "";
|
|
6903
7024
|
for (const filePath of filePaths) {
|
|
6904
|
-
const fullPath =
|
|
7025
|
+
const fullPath = path9.join(workingDir, filePath);
|
|
6905
7026
|
try {
|
|
6906
|
-
const content = await
|
|
7027
|
+
const content = await fs13.readFile(fullPath, "utf-8");
|
|
6907
7028
|
filesSection += `
|
|
6908
7029
|
|
|
6909
7030
|
=== ${filePath} ===
|
|
@@ -6937,7 +7058,7 @@ ${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
|
|
|
6937
7058
|
const color = entry.score >= 8 ? chalk12.green : entry.score >= 6 ? chalk12.yellow : chalk12.red;
|
|
6938
7059
|
const impactTag = entry.impactLevel ? chalk12.gray(` \u5F71\u54CD:${entry.impactLevel === "\u9AD8" ? chalk12.red(entry.impactLevel) : entry.impactLevel === "\u4E2D" ? chalk12.yellow(entry.impactLevel) : chalk12.green(entry.impactLevel)}`) : "";
|
|
6939
7060
|
const complexityTag = entry.complexityLevel ? chalk12.gray(` \u590D\u6742\u5EA6:${entry.complexityLevel === "\u9AD8" ? chalk12.red(entry.complexityLevel) : entry.complexityLevel === "\u4E2D" ? chalk12.yellow(entry.complexityLevel) : chalk12.green(entry.complexityLevel)}`) : "";
|
|
6940
|
-
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${
|
|
7061
|
+
console.log(` ${entry.date} [${color(bar)}] ${color(entry.score + "/10")}${impactTag}${complexityTag} ${path9.basename(entry.specFile)}`);
|
|
6941
7062
|
}
|
|
6942
7063
|
console.log(chalk12.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"));
|
|
6943
7064
|
}
|
|
@@ -6990,8 +7111,8 @@ function parseSpecAndTasks(raw) {
|
|
|
6990
7111
|
|
|
6991
7112
|
// git/worktree.ts
|
|
6992
7113
|
import { execSync as execSync4 } from "child_process";
|
|
6993
|
-
import * as
|
|
6994
|
-
import * as
|
|
7114
|
+
import * as path10 from "path";
|
|
7115
|
+
import * as fs14 from "fs-extra";
|
|
6995
7116
|
import chalk13 from "chalk";
|
|
6996
7117
|
var GitWorktreeManager = class {
|
|
6997
7118
|
constructor(baseDir) {
|
|
@@ -7017,12 +7138,12 @@ var GitWorktreeManager = class {
|
|
|
7017
7138
|
async linkDependencies(worktreePath) {
|
|
7018
7139
|
const candidates = ["node_modules", "vendor"];
|
|
7019
7140
|
for (const dir of candidates) {
|
|
7020
|
-
const src =
|
|
7021
|
-
const dest =
|
|
7022
|
-
if (!await
|
|
7023
|
-
if (await
|
|
7141
|
+
const src = path10.join(this.baseDir, dir);
|
|
7142
|
+
const dest = path10.join(worktreePath, dir);
|
|
7143
|
+
if (!await fs14.pathExists(src)) continue;
|
|
7144
|
+
if (await fs14.pathExists(dest)) continue;
|
|
7024
7145
|
try {
|
|
7025
|
-
await
|
|
7146
|
+
await fs14.ensureSymlink(src, dest, "dir");
|
|
7026
7147
|
console.log(chalk13.gray(` Symlinked ${dir}/ from base repo \u2192 worktree`));
|
|
7027
7148
|
} catch (err) {
|
|
7028
7149
|
console.log(chalk13.yellow(` \u26A0 Could not symlink ${dir}/: ${err.message}`));
|
|
@@ -7037,11 +7158,11 @@ var GitWorktreeManager = class {
|
|
|
7037
7158
|
}
|
|
7038
7159
|
const featureName = this.sanitizeFeatureName(idea);
|
|
7039
7160
|
const branchName = `feature/${featureName}`;
|
|
7040
|
-
const repoName =
|
|
7041
|
-
const worktreePath =
|
|
7161
|
+
const repoName = path10.basename(this.baseDir);
|
|
7162
|
+
const worktreePath = path10.resolve(this.baseDir, "..", `${repoName}-${featureName}`);
|
|
7042
7163
|
console.log(chalk13.cyan(`
|
|
7043
7164
|
--- Setting up Git Worktree ---`));
|
|
7044
|
-
if (await
|
|
7165
|
+
if (await fs14.pathExists(worktreePath)) {
|
|
7045
7166
|
console.log(chalk13.yellow(`\u26A0\uFE0F Worktree directory already exists at: ${worktreePath}`));
|
|
7046
7167
|
await this.linkDependencies(worktreePath);
|
|
7047
7168
|
return worktreePath;
|