ai-spec-dev 0.35.0 → 0.37.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/RELEASE_LOG.md +139 -0
- package/cli/commands/config.ts +18 -0
- package/cli/commands/create.ts +16 -1
- package/cli/utils.ts +4 -0
- package/core/code-generator.ts +6 -4
- package/core/dsl-extractor.ts +9 -1
- package/core/dsl-feedback.ts +7 -1
- package/core/dsl-validator.ts +32 -0
- package/core/key-store.ts +5 -4
- package/core/provider-utils.ts +39 -4
- package/dist/cli/index.js +121 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +122 -15
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +77 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +77 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/tests/code-generator.test.ts +253 -0
- package/tests/context-loader.test.ts +207 -0
- package/tests/dsl-validator.test.ts +105 -0
- package/tests/mock-server-generator.test.ts +404 -0
- package/tests/openapi-exporter.test.ts +310 -0
- package/tests/reviewer.test.ts +214 -0
- package/tests/spec-generator.test.ts +228 -0
- package/tests/spec-versioning.test.ts +205 -0
- package/tests/types-generator.test.ts +347 -0
- package/tests/vcr.test.ts +355 -0
- package/.claude/commands/add-lesson.md +0 -34
- package/.claude/commands/check-layers.md +0 -65
- package/.claude/commands/installed-deps.md +0 -35
- package/.claude/commands/recall-lessons.md +0 -40
- package/.claude/commands/scan-singletons.md +0 -45
- package/.claude/commands/verify-imports.md +0 -48
- package/.claude/settings.local.json +0 -24
package/dist/cli/index.js
CHANGED
|
@@ -152,14 +152,49 @@ function classifyError(err, label) {
|
|
|
152
152
|
const e = err;
|
|
153
153
|
const status = e.status ?? e.response?.status;
|
|
154
154
|
if (status === 401 || status === 403)
|
|
155
|
-
return new ProviderError(
|
|
155
|
+
return new ProviderError(
|
|
156
|
+
`Auth error (${label}): API key is invalid or expired.
|
|
157
|
+
\u2192 Check that the correct API key is set in your environment or ~/.ai-spec-keys.json
|
|
158
|
+
\u2192 Run "ai-spec model" to reconfigure your provider and key`,
|
|
159
|
+
"auth",
|
|
160
|
+
err
|
|
161
|
+
);
|
|
156
162
|
if (status === 429)
|
|
157
|
-
return new ProviderError(
|
|
163
|
+
return new ProviderError(
|
|
164
|
+
`Rate limit hit (${label}): too many requests.
|
|
165
|
+
\u2192 Wait a few minutes and retry, or switch to a different provider/model
|
|
166
|
+
\u2192 Check your provider's billing dashboard for quota status`,
|
|
167
|
+
"rate_limit",
|
|
168
|
+
err
|
|
169
|
+
);
|
|
158
170
|
if (e._timeout || e.message?.toLowerCase().includes("timed out"))
|
|
159
171
|
return new ProviderError(`Request timed out (${label})`, "timeout", err);
|
|
160
172
|
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
|
|
161
|
-
return new ProviderError(
|
|
162
|
-
|
|
173
|
+
return new ProviderError(
|
|
174
|
+
`Network error (${label}): ${e.message}
|
|
175
|
+
\u2192 Check your internet connection and proxy settings (HTTPS_PROXY)
|
|
176
|
+
\u2192 If behind a firewall, ensure the provider's API endpoint is reachable`,
|
|
177
|
+
"network",
|
|
178
|
+
err
|
|
179
|
+
);
|
|
180
|
+
const msg = e.message ?? "";
|
|
181
|
+
if (status === 404 || msg.includes("model") && (msg.includes("not found") || msg.includes("does not exist")))
|
|
182
|
+
return new ProviderError(
|
|
183
|
+
`Model not found (${label}): ${msg}
|
|
184
|
+
\u2192 Run "ai-spec model" to see available models for your provider
|
|
185
|
+
\u2192 The model name may have changed \u2014 check your provider's documentation`,
|
|
186
|
+
"provider",
|
|
187
|
+
err
|
|
188
|
+
);
|
|
189
|
+
if (msg.includes("insufficient") || msg.includes("quota") || msg.includes("balance"))
|
|
190
|
+
return new ProviderError(
|
|
191
|
+
`Quota/balance error (${label}): ${msg}
|
|
192
|
+
\u2192 Check your provider's billing dashboard
|
|
193
|
+
\u2192 Consider switching to a different provider with "ai-spec model"`,
|
|
194
|
+
"provider",
|
|
195
|
+
err
|
|
196
|
+
);
|
|
197
|
+
return new ProviderError(`Provider error (${label}): ${msg}`, "provider", err);
|
|
163
198
|
}
|
|
164
199
|
function isRetryable(err) {
|
|
165
200
|
const e = err;
|
|
@@ -4813,6 +4848,21 @@ function validateDsl(raw) {
|
|
|
4813
4848
|
for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
|
|
4814
4849
|
validateEndpoint(eps[i], `endpoints[${i}]`, errors);
|
|
4815
4850
|
}
|
|
4851
|
+
const seenEpIds = /* @__PURE__ */ new Set();
|
|
4852
|
+
for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
|
|
4853
|
+
const ep = eps[i];
|
|
4854
|
+
if (ep && typeof ep === "object" && typeof ep["id"] === "string") {
|
|
4855
|
+
const id = ep["id"];
|
|
4856
|
+
if (seenEpIds.has(id)) {
|
|
4857
|
+
errors.push({
|
|
4858
|
+
path: `endpoints[${i}].id`,
|
|
4859
|
+
message: `Duplicate endpoint id "${id}" \u2014 each endpoint must have a unique id`
|
|
4860
|
+
});
|
|
4861
|
+
} else {
|
|
4862
|
+
seenEpIds.add(id);
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4816
4866
|
}
|
|
4817
4867
|
if (obj["behaviors"] !== void 0) {
|
|
4818
4868
|
if (!Array.isArray(obj["behaviors"])) {
|
|
@@ -4869,6 +4919,21 @@ function validateModel(raw, path40, errors) {
|
|
|
4869
4919
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4870
4920
|
validateModelField(fields[j2], `${path40}.fields[${j2}]`, errors);
|
|
4871
4921
|
}
|
|
4922
|
+
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
4923
|
+
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4924
|
+
const f = fields[j2];
|
|
4925
|
+
if (f && typeof f === "object" && typeof f["name"] === "string") {
|
|
4926
|
+
const name = f["name"];
|
|
4927
|
+
if (seenFieldNames.has(name)) {
|
|
4928
|
+
errors.push({
|
|
4929
|
+
path: `${path40}.fields[${j2}].name`,
|
|
4930
|
+
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
4931
|
+
});
|
|
4932
|
+
} else {
|
|
4933
|
+
seenFieldNames.add(name);
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4872
4937
|
}
|
|
4873
4938
|
if (m["relations"] !== void 0) {
|
|
4874
4939
|
if (!Array.isArray(m["relations"])) {
|
|
@@ -5336,7 +5401,10 @@ var DslExtractor = class {
|
|
|
5336
5401
|
* - throws if user chose to abort
|
|
5337
5402
|
*/
|
|
5338
5403
|
async extract(specContent, opts = {}) {
|
|
5339
|
-
const specForAI = specContent.length > MAX_SPEC_CHARS ?
|
|
5404
|
+
const specForAI = specContent.length > MAX_SPEC_CHARS ? (() => {
|
|
5405
|
+
console.log(import_chalk6.default.yellow(` \u26A0 Spec is ${specContent.length} chars \u2014 truncating to ${MAX_SPEC_CHARS} for DSL extraction. Details at the end may be lost.`));
|
|
5406
|
+
return specContent.slice(0, MAX_SPEC_CHARS) + "\n... (truncated for DSL extraction)";
|
|
5407
|
+
})() : specContent;
|
|
5340
5408
|
let lastRawOutput = "";
|
|
5341
5409
|
let lastErrors = [];
|
|
5342
5410
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -5360,6 +5428,11 @@ var DslExtractor = class {
|
|
|
5360
5428
|
parsed = parseJsonFromOutput(rawOutput);
|
|
5361
5429
|
} catch (parseErr) {
|
|
5362
5430
|
console.log(import_chalk6.default.red(` \u2718 Failed to parse JSON from AI output: ${parseErr.message}`));
|
|
5431
|
+
const preview = rawOutput.slice(0, 500).replace(/\n/g, "\\n");
|
|
5432
|
+
console.log(import_chalk6.default.gray(` AI output preview (first 500 chars): ${preview}`));
|
|
5433
|
+
if (rawOutput.length > MAX_SPEC_CHARS) {
|
|
5434
|
+
console.log(import_chalk6.default.gray(` Note: spec was truncated to ${MAX_SPEC_CHARS} chars \u2014 long specs may lose context`));
|
|
5435
|
+
}
|
|
5363
5436
|
lastErrors = [{ path: "root", message: "Output is not valid JSON \u2014 see raw output above" }];
|
|
5364
5437
|
if (attempt < MAX_RETRIES) continue;
|
|
5365
5438
|
return this.handleFailure(opts, "AI produced invalid JSON after retries");
|
|
@@ -6468,9 +6541,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
6468
6541
|
console.log(import_chalk8.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6469
6542
|
console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
|
|
6470
6543
|
try {
|
|
6471
|
-
(0, import_child_process.
|
|
6544
|
+
(0, import_child_process.spawnSync)(claudeCmd, ["-p", promptContent], {
|
|
6472
6545
|
cwd: workingDir,
|
|
6473
|
-
stdio: "inherit"
|
|
6546
|
+
stdio: "inherit",
|
|
6547
|
+
shell: false
|
|
6474
6548
|
});
|
|
6475
6549
|
console.log(import_chalk8.default.green("\n \u2714 Claude Code completed."));
|
|
6476
6550
|
} catch {
|
|
@@ -6522,9 +6596,10 @@ Full spec is at: ${specFilePath}
|
|
|
6522
6596
|
Implement ONLY this task. Do not implement other tasks.`;
|
|
6523
6597
|
let taskStatus = "done";
|
|
6524
6598
|
try {
|
|
6525
|
-
(0, import_child_process.
|
|
6599
|
+
(0, import_child_process.spawnSync)(claudeCmd, ["-p", taskPrompt], {
|
|
6526
6600
|
cwd: workingDir,
|
|
6527
|
-
stdio: "inherit"
|
|
6601
|
+
stdio: "inherit",
|
|
6602
|
+
shell: false
|
|
6528
6603
|
});
|
|
6529
6604
|
completed++;
|
|
6530
6605
|
} catch {
|
|
@@ -9306,7 +9381,7 @@ function assessDslRichness(dsl) {
|
|
|
9306
9381
|
const endpointsWithoutErrors = dsl.endpoints.filter(
|
|
9307
9382
|
(ep) => !ep.errors || ep.errors.length === 0
|
|
9308
9383
|
);
|
|
9309
|
-
if (endpointsWithoutErrors.length
|
|
9384
|
+
if (endpointsWithoutErrors.length === dsl.endpoints.length && dsl.endpoints.length >= 2) {
|
|
9310
9385
|
gaps.push({
|
|
9311
9386
|
code: "missing_errors",
|
|
9312
9387
|
message: `${endpointsWithoutErrors.length}/${dsl.endpoints.length} endpoints have no error definitions`,
|
|
@@ -9892,13 +9967,15 @@ async function readStore() {
|
|
|
9892
9967
|
if (await fs19.pathExists(KEY_STORE_FILE)) {
|
|
9893
9968
|
return await fs19.readJson(KEY_STORE_FILE);
|
|
9894
9969
|
}
|
|
9895
|
-
} catch {
|
|
9970
|
+
} catch (err) {
|
|
9971
|
+
console.warn(`Warning: Could not read key store at ${KEY_STORE_FILE}: ${err.message}. Using empty store.`);
|
|
9896
9972
|
}
|
|
9897
9973
|
return {};
|
|
9898
9974
|
}
|
|
9899
9975
|
async function writeStore(store) {
|
|
9900
|
-
await fs19.
|
|
9976
|
+
await fs19.ensureFile(KEY_STORE_FILE);
|
|
9901
9977
|
await fs19.chmod(KEY_STORE_FILE, 384);
|
|
9978
|
+
await fs19.writeJson(KEY_STORE_FILE, store, { spaces: 2 });
|
|
9902
9979
|
}
|
|
9903
9980
|
async function getSavedKey(provider) {
|
|
9904
9981
|
const store = await readStore();
|
|
@@ -10991,8 +11068,10 @@ function registerCreate(program2) {
|
|
|
10991
11068
|
console.log(import_chalk20.default.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10992
11069
|
}
|
|
10993
11070
|
runLogger.stageStart("error_feedback");
|
|
11071
|
+
const defaultCycles = opts.tdd ? 3 : 2;
|
|
11072
|
+
const maxCycles = config2.maxErrorCycles ?? defaultCycles;
|
|
10994
11073
|
compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
10995
|
-
maxCycles
|
|
11074
|
+
maxCycles
|
|
10996
11075
|
});
|
|
10997
11076
|
runLogger.stageEnd("error_feedback");
|
|
10998
11077
|
}
|
|
@@ -11095,6 +11174,18 @@ function registerCreate(program2) {
|
|
|
11095
11174
|
logger: runLogger
|
|
11096
11175
|
});
|
|
11097
11176
|
printSelfEval(selfEvalResult);
|
|
11177
|
+
const minHarness = config2.minHarnessScore ?? 0;
|
|
11178
|
+
if (minHarness > 0 && selfEvalResult.harnessScore < minHarness && !opts.force) {
|
|
11179
|
+
console.log(import_chalk20.default.red(
|
|
11180
|
+
`
|
|
11181
|
+
\u2718 Harness score ${selfEvalResult.harnessScore}/10 is below the minimum threshold ${minHarness}/10.`
|
|
11182
|
+
));
|
|
11183
|
+
console.log(import_chalk20.default.gray(` Gate threshold set in .ai-spec.json \u2192 "minHarnessScore": ${minHarness}`));
|
|
11184
|
+
console.log(import_chalk20.default.gray(` Use --force to bypass, or improve the spec and re-run.`));
|
|
11185
|
+
runLogger.stageEnd("self_eval", { gateBlocked: true, score: selfEvalResult.harnessScore, threshold: minHarness });
|
|
11186
|
+
runLogger.finish();
|
|
11187
|
+
process.exit(1);
|
|
11188
|
+
}
|
|
11098
11189
|
if (accumulatePromise) await accumulatePromise;
|
|
11099
11190
|
if (specVcrRecorder) {
|
|
11100
11191
|
const vcrPath = await specVcrRecorder.save(currentDir, runId, codegenVcrRecorder ?? void 0);
|
|
@@ -11666,7 +11757,7 @@ var path26 = __toESM(require("path"));
|
|
|
11666
11757
|
var fs27 = __toESM(require("fs-extra"));
|
|
11667
11758
|
var import_chalk24 = __toESM(require("chalk"));
|
|
11668
11759
|
function registerConfig(program2) {
|
|
11669
|
-
program2.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)").option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
11760
|
+
program2.command("config").description(`Set default configuration for this project (saved to ${CONFIG_FILE})`).option("--provider <name>", "Default AI provider for spec generation").option("--model <name>", "Default model for spec generation").option("--codegen <mode>", "Default code generation mode (claude-code|api|plan)").option("--codegen-provider <name>", "Default provider for code generation").option("--codegen-model <name>", "Default model for code generation").option("--min-spec-score <score>", "Minimum overall spec score (1-10) to pass Approval Gate (0 = disabled)").option("--min-harness-score <score>", "Minimum harness score (1-10) for pipeline success (0 = disabled)").option("--max-error-cycles <n>", "Maximum error-feedback fix cycles (1-10, default: 2)").option("--show", "Print current configuration").option("--reset", "Reset configuration to empty").option("--clear-keys", "Delete all saved API keys from ~/.ai-spec-keys.json").option("--clear-key <provider>", "Delete saved API key for a specific provider").option("--list-keys", "Show which providers have a saved key").action(async (opts) => {
|
|
11670
11761
|
const currentDir = process.cwd();
|
|
11671
11762
|
const configPath = path26.join(currentDir, CONFIG_FILE);
|
|
11672
11763
|
if (opts.clearKeys) {
|
|
@@ -11724,6 +11815,22 @@ File: ${KEY_STORE_FILE}`));
|
|
|
11724
11815
|
}
|
|
11725
11816
|
updated.minSpecScore = score;
|
|
11726
11817
|
}
|
|
11818
|
+
if (opts.minHarnessScore !== void 0) {
|
|
11819
|
+
const score = parseInt(opts.minHarnessScore, 10);
|
|
11820
|
+
if (isNaN(score) || score < 0 || score > 10) {
|
|
11821
|
+
console.error(import_chalk24.default.red(" --min-harness-score must be a number between 0 and 10"));
|
|
11822
|
+
process.exit(1);
|
|
11823
|
+
}
|
|
11824
|
+
updated.minHarnessScore = score;
|
|
11825
|
+
}
|
|
11826
|
+
if (opts.maxErrorCycles !== void 0) {
|
|
11827
|
+
const cycles = parseInt(opts.maxErrorCycles, 10);
|
|
11828
|
+
if (isNaN(cycles) || cycles < 1 || cycles > 10) {
|
|
11829
|
+
console.error(import_chalk24.default.red(" --max-error-cycles must be a number between 1 and 10"));
|
|
11830
|
+
process.exit(1);
|
|
11831
|
+
}
|
|
11832
|
+
updated.maxErrorCycles = cycles;
|
|
11833
|
+
}
|
|
11727
11834
|
await fs27.writeJson(configPath, updated, { spaces: 2 });
|
|
11728
11835
|
console.log(import_chalk24.default.green(`\u2714 Config saved to ${configPath}`));
|
|
11729
11836
|
console.log(JSON.stringify(updated, null, 2));
|