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.mjs
CHANGED
|
@@ -132,14 +132,49 @@ function classifyError(err, label) {
|
|
|
132
132
|
const e = err;
|
|
133
133
|
const status = e.status ?? e.response?.status;
|
|
134
134
|
if (status === 401 || status === 403)
|
|
135
|
-
return new ProviderError(
|
|
135
|
+
return new ProviderError(
|
|
136
|
+
`Auth error (${label}): API key is invalid or expired.
|
|
137
|
+
\u2192 Check that the correct API key is set in your environment or ~/.ai-spec-keys.json
|
|
138
|
+
\u2192 Run "ai-spec model" to reconfigure your provider and key`,
|
|
139
|
+
"auth",
|
|
140
|
+
err
|
|
141
|
+
);
|
|
136
142
|
if (status === 429)
|
|
137
|
-
return new ProviderError(
|
|
143
|
+
return new ProviderError(
|
|
144
|
+
`Rate limit hit (${label}): too many requests.
|
|
145
|
+
\u2192 Wait a few minutes and retry, or switch to a different provider/model
|
|
146
|
+
\u2192 Check your provider's billing dashboard for quota status`,
|
|
147
|
+
"rate_limit",
|
|
148
|
+
err
|
|
149
|
+
);
|
|
138
150
|
if (e._timeout || e.message?.toLowerCase().includes("timed out"))
|
|
139
151
|
return new ProviderError(`Request timed out (${label})`, "timeout", err);
|
|
140
152
|
if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
|
|
141
|
-
return new ProviderError(
|
|
142
|
-
|
|
153
|
+
return new ProviderError(
|
|
154
|
+
`Network error (${label}): ${e.message}
|
|
155
|
+
\u2192 Check your internet connection and proxy settings (HTTPS_PROXY)
|
|
156
|
+
\u2192 If behind a firewall, ensure the provider's API endpoint is reachable`,
|
|
157
|
+
"network",
|
|
158
|
+
err
|
|
159
|
+
);
|
|
160
|
+
const msg = e.message ?? "";
|
|
161
|
+
if (status === 404 || msg.includes("model") && (msg.includes("not found") || msg.includes("does not exist")))
|
|
162
|
+
return new ProviderError(
|
|
163
|
+
`Model not found (${label}): ${msg}
|
|
164
|
+
\u2192 Run "ai-spec model" to see available models for your provider
|
|
165
|
+
\u2192 The model name may have changed \u2014 check your provider's documentation`,
|
|
166
|
+
"provider",
|
|
167
|
+
err
|
|
168
|
+
);
|
|
169
|
+
if (msg.includes("insufficient") || msg.includes("quota") || msg.includes("balance"))
|
|
170
|
+
return new ProviderError(
|
|
171
|
+
`Quota/balance error (${label}): ${msg}
|
|
172
|
+
\u2192 Check your provider's billing dashboard
|
|
173
|
+
\u2192 Consider switching to a different provider with "ai-spec model"`,
|
|
174
|
+
"provider",
|
|
175
|
+
err
|
|
176
|
+
);
|
|
177
|
+
return new ProviderError(`Provider error (${label}): ${msg}`, "provider", err);
|
|
143
178
|
}
|
|
144
179
|
function isRetryable(err) {
|
|
145
180
|
const e = err;
|
|
@@ -4165,7 +4200,7 @@ ${currentSpec}`,
|
|
|
4165
4200
|
|
|
4166
4201
|
// core/code-generator.ts
|
|
4167
4202
|
import chalk8 from "chalk";
|
|
4168
|
-
import { execSync } from "child_process";
|
|
4203
|
+
import { execSync, spawnSync } from "child_process";
|
|
4169
4204
|
import * as path9 from "path";
|
|
4170
4205
|
import * as fs10 from "fs-extra";
|
|
4171
4206
|
|
|
@@ -4792,6 +4827,21 @@ function validateDsl(raw) {
|
|
|
4792
4827
|
for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
|
|
4793
4828
|
validateEndpoint(eps[i], `endpoints[${i}]`, errors);
|
|
4794
4829
|
}
|
|
4830
|
+
const seenEpIds = /* @__PURE__ */ new Set();
|
|
4831
|
+
for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
|
|
4832
|
+
const ep = eps[i];
|
|
4833
|
+
if (ep && typeof ep === "object" && typeof ep["id"] === "string") {
|
|
4834
|
+
const id = ep["id"];
|
|
4835
|
+
if (seenEpIds.has(id)) {
|
|
4836
|
+
errors.push({
|
|
4837
|
+
path: `endpoints[${i}].id`,
|
|
4838
|
+
message: `Duplicate endpoint id "${id}" \u2014 each endpoint must have a unique id`
|
|
4839
|
+
});
|
|
4840
|
+
} else {
|
|
4841
|
+
seenEpIds.add(id);
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4795
4845
|
}
|
|
4796
4846
|
if (obj["behaviors"] !== void 0) {
|
|
4797
4847
|
if (!Array.isArray(obj["behaviors"])) {
|
|
@@ -4848,6 +4898,21 @@ function validateModel(raw, path40, errors) {
|
|
|
4848
4898
|
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4849
4899
|
validateModelField(fields[j2], `${path40}.fields[${j2}]`, errors);
|
|
4850
4900
|
}
|
|
4901
|
+
const seenFieldNames = /* @__PURE__ */ new Set();
|
|
4902
|
+
for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
|
|
4903
|
+
const f = fields[j2];
|
|
4904
|
+
if (f && typeof f === "object" && typeof f["name"] === "string") {
|
|
4905
|
+
const name = f["name"];
|
|
4906
|
+
if (seenFieldNames.has(name)) {
|
|
4907
|
+
errors.push({
|
|
4908
|
+
path: `${path40}.fields[${j2}].name`,
|
|
4909
|
+
message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
|
|
4910
|
+
});
|
|
4911
|
+
} else {
|
|
4912
|
+
seenFieldNames.add(name);
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4851
4916
|
}
|
|
4852
4917
|
if (m["relations"] !== void 0) {
|
|
4853
4918
|
if (!Array.isArray(m["relations"])) {
|
|
@@ -5315,7 +5380,10 @@ var DslExtractor = class {
|
|
|
5315
5380
|
* - throws if user chose to abort
|
|
5316
5381
|
*/
|
|
5317
5382
|
async extract(specContent, opts = {}) {
|
|
5318
|
-
const specForAI = specContent.length > MAX_SPEC_CHARS ?
|
|
5383
|
+
const specForAI = specContent.length > MAX_SPEC_CHARS ? (() => {
|
|
5384
|
+
console.log(chalk6.yellow(` \u26A0 Spec is ${specContent.length} chars \u2014 truncating to ${MAX_SPEC_CHARS} for DSL extraction. Details at the end may be lost.`));
|
|
5385
|
+
return specContent.slice(0, MAX_SPEC_CHARS) + "\n... (truncated for DSL extraction)";
|
|
5386
|
+
})() : specContent;
|
|
5319
5387
|
let lastRawOutput = "";
|
|
5320
5388
|
let lastErrors = [];
|
|
5321
5389
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -5339,6 +5407,11 @@ var DslExtractor = class {
|
|
|
5339
5407
|
parsed = parseJsonFromOutput(rawOutput);
|
|
5340
5408
|
} catch (parseErr) {
|
|
5341
5409
|
console.log(chalk6.red(` \u2718 Failed to parse JSON from AI output: ${parseErr.message}`));
|
|
5410
|
+
const preview = rawOutput.slice(0, 500).replace(/\n/g, "\\n");
|
|
5411
|
+
console.log(chalk6.gray(` AI output preview (first 500 chars): ${preview}`));
|
|
5412
|
+
if (rawOutput.length > MAX_SPEC_CHARS) {
|
|
5413
|
+
console.log(chalk6.gray(` Note: spec was truncated to ${MAX_SPEC_CHARS} chars \u2014 long specs may lose context`));
|
|
5414
|
+
}
|
|
5342
5415
|
lastErrors = [{ path: "root", message: "Output is not valid JSON \u2014 see raw output above" }];
|
|
5343
5416
|
if (attempt < MAX_RETRIES) continue;
|
|
5344
5417
|
return this.handleFailure(opts, "AI produced invalid JSON after retries");
|
|
@@ -6447,9 +6520,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
|
|
|
6447
6520
|
console.log(chalk8.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
|
|
6448
6521
|
console.log(chalk8.gray(` Spec: ${specFilePath}`));
|
|
6449
6522
|
try {
|
|
6450
|
-
|
|
6523
|
+
spawnSync(claudeCmd, ["-p", promptContent], {
|
|
6451
6524
|
cwd: workingDir,
|
|
6452
|
-
stdio: "inherit"
|
|
6525
|
+
stdio: "inherit",
|
|
6526
|
+
shell: false
|
|
6453
6527
|
});
|
|
6454
6528
|
console.log(chalk8.green("\n \u2714 Claude Code completed."));
|
|
6455
6529
|
} catch {
|
|
@@ -6501,9 +6575,10 @@ Full spec is at: ${specFilePath}
|
|
|
6501
6575
|
Implement ONLY this task. Do not implement other tasks.`;
|
|
6502
6576
|
let taskStatus = "done";
|
|
6503
6577
|
try {
|
|
6504
|
-
|
|
6578
|
+
spawnSync(claudeCmd, ["-p", taskPrompt], {
|
|
6505
6579
|
cwd: workingDir,
|
|
6506
|
-
stdio: "inherit"
|
|
6580
|
+
stdio: "inherit",
|
|
6581
|
+
shell: false
|
|
6507
6582
|
});
|
|
6508
6583
|
completed++;
|
|
6509
6584
|
} catch {
|
|
@@ -9285,7 +9360,7 @@ function assessDslRichness(dsl) {
|
|
|
9285
9360
|
const endpointsWithoutErrors = dsl.endpoints.filter(
|
|
9286
9361
|
(ep) => !ep.errors || ep.errors.length === 0
|
|
9287
9362
|
);
|
|
9288
|
-
if (endpointsWithoutErrors.length
|
|
9363
|
+
if (endpointsWithoutErrors.length === dsl.endpoints.length && dsl.endpoints.length >= 2) {
|
|
9289
9364
|
gaps.push({
|
|
9290
9365
|
code: "missing_errors",
|
|
9291
9366
|
message: `${endpointsWithoutErrors.length}/${dsl.endpoints.length} endpoints have no error definitions`,
|
|
@@ -9871,13 +9946,15 @@ async function readStore() {
|
|
|
9871
9946
|
if (await fs19.pathExists(KEY_STORE_FILE)) {
|
|
9872
9947
|
return await fs19.readJson(KEY_STORE_FILE);
|
|
9873
9948
|
}
|
|
9874
|
-
} catch {
|
|
9949
|
+
} catch (err) {
|
|
9950
|
+
console.warn(`Warning: Could not read key store at ${KEY_STORE_FILE}: ${err.message}. Using empty store.`);
|
|
9875
9951
|
}
|
|
9876
9952
|
return {};
|
|
9877
9953
|
}
|
|
9878
9954
|
async function writeStore(store) {
|
|
9879
|
-
await fs19.
|
|
9955
|
+
await fs19.ensureFile(KEY_STORE_FILE);
|
|
9880
9956
|
await fs19.chmod(KEY_STORE_FILE, 384);
|
|
9957
|
+
await fs19.writeJson(KEY_STORE_FILE, store, { spaces: 2 });
|
|
9881
9958
|
}
|
|
9882
9959
|
async function getSavedKey(provider) {
|
|
9883
9960
|
const store = await readStore();
|
|
@@ -10970,8 +11047,10 @@ function registerCreate(program2) {
|
|
|
10970
11047
|
console.log(chalk20.cyan("[8/9] TDD mode \u2014 error feedback loop driving implementation to pass tests..."));
|
|
10971
11048
|
}
|
|
10972
11049
|
runLogger.stageStart("error_feedback");
|
|
11050
|
+
const defaultCycles = opts.tdd ? 3 : 2;
|
|
11051
|
+
const maxCycles = config2.maxErrorCycles ?? defaultCycles;
|
|
10973
11052
|
compilePassed = await runErrorFeedback(codegenProvider, workingDir, extractedDsl, {
|
|
10974
|
-
maxCycles
|
|
11053
|
+
maxCycles
|
|
10975
11054
|
});
|
|
10976
11055
|
runLogger.stageEnd("error_feedback");
|
|
10977
11056
|
}
|
|
@@ -11074,6 +11153,18 @@ function registerCreate(program2) {
|
|
|
11074
11153
|
logger: runLogger
|
|
11075
11154
|
});
|
|
11076
11155
|
printSelfEval(selfEvalResult);
|
|
11156
|
+
const minHarness = config2.minHarnessScore ?? 0;
|
|
11157
|
+
if (minHarness > 0 && selfEvalResult.harnessScore < minHarness && !opts.force) {
|
|
11158
|
+
console.log(chalk20.red(
|
|
11159
|
+
`
|
|
11160
|
+
\u2718 Harness score ${selfEvalResult.harnessScore}/10 is below the minimum threshold ${minHarness}/10.`
|
|
11161
|
+
));
|
|
11162
|
+
console.log(chalk20.gray(` Gate threshold set in .ai-spec.json \u2192 "minHarnessScore": ${minHarness}`));
|
|
11163
|
+
console.log(chalk20.gray(` Use --force to bypass, or improve the spec and re-run.`));
|
|
11164
|
+
runLogger.stageEnd("self_eval", { gateBlocked: true, score: selfEvalResult.harnessScore, threshold: minHarness });
|
|
11165
|
+
runLogger.finish();
|
|
11166
|
+
process.exit(1);
|
|
11167
|
+
}
|
|
11077
11168
|
if (accumulatePromise) await accumulatePromise;
|
|
11078
11169
|
if (specVcrRecorder) {
|
|
11079
11170
|
const vcrPath = await specVcrRecorder.save(currentDir, runId, codegenVcrRecorder ?? void 0);
|
|
@@ -11645,7 +11736,7 @@ import * as path26 from "path";
|
|
|
11645
11736
|
import * as fs27 from "fs-extra";
|
|
11646
11737
|
import chalk24 from "chalk";
|
|
11647
11738
|
function registerConfig(program2) {
|
|
11648
|
-
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) => {
|
|
11739
|
+
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) => {
|
|
11649
11740
|
const currentDir = process.cwd();
|
|
11650
11741
|
const configPath = path26.join(currentDir, CONFIG_FILE);
|
|
11651
11742
|
if (opts.clearKeys) {
|
|
@@ -11703,6 +11794,22 @@ File: ${KEY_STORE_FILE}`));
|
|
|
11703
11794
|
}
|
|
11704
11795
|
updated.minSpecScore = score;
|
|
11705
11796
|
}
|
|
11797
|
+
if (opts.minHarnessScore !== void 0) {
|
|
11798
|
+
const score = parseInt(opts.minHarnessScore, 10);
|
|
11799
|
+
if (isNaN(score) || score < 0 || score > 10) {
|
|
11800
|
+
console.error(chalk24.red(" --min-harness-score must be a number between 0 and 10"));
|
|
11801
|
+
process.exit(1);
|
|
11802
|
+
}
|
|
11803
|
+
updated.minHarnessScore = score;
|
|
11804
|
+
}
|
|
11805
|
+
if (opts.maxErrorCycles !== void 0) {
|
|
11806
|
+
const cycles = parseInt(opts.maxErrorCycles, 10);
|
|
11807
|
+
if (isNaN(cycles) || cycles < 1 || cycles > 10) {
|
|
11808
|
+
console.error(chalk24.red(" --max-error-cycles must be a number between 1 and 10"));
|
|
11809
|
+
process.exit(1);
|
|
11810
|
+
}
|
|
11811
|
+
updated.maxErrorCycles = cycles;
|
|
11812
|
+
}
|
|
11706
11813
|
await fs27.writeJson(configPath, updated, { spaces: 2 });
|
|
11707
11814
|
console.log(chalk24.green(`\u2714 Config saved to ${configPath}`));
|
|
11708
11815
|
console.log(JSON.stringify(updated, null, 2));
|