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.
Files changed (38) hide show
  1. package/RELEASE_LOG.md +139 -0
  2. package/cli/commands/config.ts +18 -0
  3. package/cli/commands/create.ts +16 -1
  4. package/cli/utils.ts +4 -0
  5. package/core/code-generator.ts +6 -4
  6. package/core/dsl-extractor.ts +9 -1
  7. package/core/dsl-feedback.ts +7 -1
  8. package/core/dsl-validator.ts +32 -0
  9. package/core/key-store.ts +5 -4
  10. package/core/provider-utils.ts +39 -4
  11. package/dist/cli/index.js +121 -14
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/index.mjs +122 -15
  14. package/dist/cli/index.mjs.map +1 -1
  15. package/dist/index.d.mts +16 -1
  16. package/dist/index.d.ts +16 -1
  17. package/dist/index.js +77 -8
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +77 -9
  20. package/dist/index.mjs.map +1 -1
  21. package/package.json +1 -1
  22. package/tests/code-generator.test.ts +253 -0
  23. package/tests/context-loader.test.ts +207 -0
  24. package/tests/dsl-validator.test.ts +105 -0
  25. package/tests/mock-server-generator.test.ts +404 -0
  26. package/tests/openapi-exporter.test.ts +310 -0
  27. package/tests/reviewer.test.ts +214 -0
  28. package/tests/spec-generator.test.ts +228 -0
  29. package/tests/spec-versioning.test.ts +205 -0
  30. package/tests/types-generator.test.ts +347 -0
  31. package/tests/vcr.test.ts +355 -0
  32. package/.claude/commands/add-lesson.md +0 -34
  33. package/.claude/commands/check-layers.md +0 -65
  34. package/.claude/commands/installed-deps.md +0 -35
  35. package/.claude/commands/recall-lessons.md +0 -40
  36. package/.claude/commands/scan-singletons.md +0 -45
  37. package/.claude/commands/verify-imports.md +0 -48
  38. package/.claude/settings.local.json +0 -24
package/dist/index.d.mts CHANGED
@@ -183,6 +183,21 @@ declare function loadTasksForSpec(specFilePath: string): Promise<SpecTask[] | nu
183
183
  /** Persist a single task's status to the tasks JSON file (checkpoint). */
184
184
  declare function updateTaskStatus(specFilePath: string, taskId: string, status: TaskStatus): Promise<void>;
185
185
 
186
+ /**
187
+ * Extract a behavioral contract summary from a generated file.
188
+ *
189
+ * Captures:
190
+ * - export interface / type / enum — full multi-line blocks (the actual TS contracts)
191
+ * - export function / const / class — opening signature line
192
+ * - Throw statements — error codes & validation constraints
193
+ *
194
+ * Multi-line blocks (interface, type alias with {}) are captured in full so
195
+ * downstream tasks see complete method signatures and field shapes, not just
196
+ * a single-line "export interface Foo {" that conveys nothing.
197
+ *
198
+ * Falls back to first 3000 chars for CommonJS files with no explicit exports.
199
+ */
200
+ declare function extractBehavioralContract(content: string): string;
186
201
  type CodeGenMode = "claude-code" | "api" | "plan";
187
202
  interface CodeGenOptions {
188
203
  /** Run claude non-interactively via -p flag (saves tokens, good for automation) */
@@ -281,4 +296,4 @@ declare class GitWorktreeManager {
281
296
  createWorktree(idea: string): Promise<string | null>;
282
297
  }
283
298
 
284
- export { type AIProvider, CONSTITUTION_FILE, ClaudeProvider, type CodeGenMode, type CodeGenOptions, CodeGenerator, CodeReviewer, ConstitutionGenerator, ContextLoader, DEFAULT_MODELS, ENV_KEY_MAP, FRONTEND_FRAMEWORKS, GeminiProvider, GitWorktreeManager, MiMoProvider, OpenAICompatibleProvider, PROVIDER_CATALOG, type ProjectContext, type ProviderMeta, SUPPORTED_PROVIDERS, type SharedConfigFile, SpecGenerator, SpecRefiner, type SpecTask, TaskGenerator, type TaskLayer, type TaskPriority, type TaskStatus, buildTaskPrompt, createProvider, extractComplianceScore, extractMissingCount, generateSpecWithTasks, isFrontendDeps, loadConstitution, loadTasksForSpec, printConstitutionHint, printTaskProgress, printTasks, updateTaskStatus };
299
+ export { type AIProvider, CONSTITUTION_FILE, ClaudeProvider, type CodeGenMode, type CodeGenOptions, CodeGenerator, CodeReviewer, ConstitutionGenerator, ContextLoader, DEFAULT_MODELS, ENV_KEY_MAP, FRONTEND_FRAMEWORKS, GeminiProvider, GitWorktreeManager, MiMoProvider, OpenAICompatibleProvider, PROVIDER_CATALOG, type ProjectContext, type ProviderMeta, SUPPORTED_PROVIDERS, type SharedConfigFile, SpecGenerator, SpecRefiner, type SpecTask, TaskGenerator, type TaskLayer, type TaskPriority, type TaskStatus, buildTaskPrompt, createProvider, extractBehavioralContract, extractComplianceScore, extractMissingCount, generateSpecWithTasks, isFrontendDeps, loadConstitution, loadTasksForSpec, printConstitutionHint, printTaskProgress, printTasks, updateTaskStatus };
package/dist/index.d.ts CHANGED
@@ -183,6 +183,21 @@ declare function loadTasksForSpec(specFilePath: string): Promise<SpecTask[] | nu
183
183
  /** Persist a single task's status to the tasks JSON file (checkpoint). */
184
184
  declare function updateTaskStatus(specFilePath: string, taskId: string, status: TaskStatus): Promise<void>;
185
185
 
186
+ /**
187
+ * Extract a behavioral contract summary from a generated file.
188
+ *
189
+ * Captures:
190
+ * - export interface / type / enum — full multi-line blocks (the actual TS contracts)
191
+ * - export function / const / class — opening signature line
192
+ * - Throw statements — error codes & validation constraints
193
+ *
194
+ * Multi-line blocks (interface, type alias with {}) are captured in full so
195
+ * downstream tasks see complete method signatures and field shapes, not just
196
+ * a single-line "export interface Foo {" that conveys nothing.
197
+ *
198
+ * Falls back to first 3000 chars for CommonJS files with no explicit exports.
199
+ */
200
+ declare function extractBehavioralContract(content: string): string;
186
201
  type CodeGenMode = "claude-code" | "api" | "plan";
187
202
  interface CodeGenOptions {
188
203
  /** Run claude non-interactively via -p flag (saves tokens, good for automation) */
@@ -281,4 +296,4 @@ declare class GitWorktreeManager {
281
296
  createWorktree(idea: string): Promise<string | null>;
282
297
  }
283
298
 
284
- export { type AIProvider, CONSTITUTION_FILE, ClaudeProvider, type CodeGenMode, type CodeGenOptions, CodeGenerator, CodeReviewer, ConstitutionGenerator, ContextLoader, DEFAULT_MODELS, ENV_KEY_MAP, FRONTEND_FRAMEWORKS, GeminiProvider, GitWorktreeManager, MiMoProvider, OpenAICompatibleProvider, PROVIDER_CATALOG, type ProjectContext, type ProviderMeta, SUPPORTED_PROVIDERS, type SharedConfigFile, SpecGenerator, SpecRefiner, type SpecTask, TaskGenerator, type TaskLayer, type TaskPriority, type TaskStatus, buildTaskPrompt, createProvider, extractComplianceScore, extractMissingCount, generateSpecWithTasks, isFrontendDeps, loadConstitution, loadTasksForSpec, printConstitutionHint, printTaskProgress, printTasks, updateTaskStatus };
299
+ export { type AIProvider, CONSTITUTION_FILE, ClaudeProvider, type CodeGenMode, type CodeGenOptions, CodeGenerator, CodeReviewer, ConstitutionGenerator, ContextLoader, DEFAULT_MODELS, ENV_KEY_MAP, FRONTEND_FRAMEWORKS, GeminiProvider, GitWorktreeManager, MiMoProvider, OpenAICompatibleProvider, PROVIDER_CATALOG, type ProjectContext, type ProviderMeta, SUPPORTED_PROVIDERS, type SharedConfigFile, SpecGenerator, SpecRefiner, type SpecTask, TaskGenerator, type TaskLayer, type TaskPriority, type TaskStatus, buildTaskPrompt, createProvider, extractBehavioralContract, extractComplianceScore, extractMissingCount, generateSpecWithTasks, isFrontendDeps, loadConstitution, loadTasksForSpec, printConstitutionHint, printTaskProgress, printTasks, updateTaskStatus };
package/dist/index.js CHANGED
@@ -50,6 +50,7 @@ __export(index_exports, {
50
50
  TaskGenerator: () => TaskGenerator,
51
51
  buildTaskPrompt: () => buildTaskPrompt,
52
52
  createProvider: () => createProvider,
53
+ extractBehavioralContract: () => extractBehavioralContract,
53
54
  extractComplianceScore: () => extractComplianceScore,
54
55
  extractMissingCount: () => extractMissingCount,
55
56
  generateSpecWithTasks: () => generateSpecWithTasks,
@@ -196,14 +197,49 @@ function classifyError(err, label) {
196
197
  const e = err;
197
198
  const status = e.status ?? e.response?.status;
198
199
  if (status === 401 || status === 403)
199
- return new ProviderError(`Auth error \u2014 check your API key (${label})`, "auth", err);
200
+ return new ProviderError(
201
+ `Auth error (${label}): API key is invalid or expired.
202
+ \u2192 Check that the correct API key is set in your environment or ~/.ai-spec-keys.json
203
+ \u2192 Run "ai-spec model" to reconfigure your provider and key`,
204
+ "auth",
205
+ err
206
+ );
200
207
  if (status === 429)
201
- return new ProviderError(`Rate limit hit (${label}) \u2014 try again later or switch provider`, "rate_limit", err);
208
+ return new ProviderError(
209
+ `Rate limit hit (${label}): too many requests.
210
+ \u2192 Wait a few minutes and retry, or switch to a different provider/model
211
+ \u2192 Check your provider's billing dashboard for quota status`,
212
+ "rate_limit",
213
+ err
214
+ );
202
215
  if (e._timeout || e.message?.toLowerCase().includes("timed out"))
203
216
  return new ProviderError(`Request timed out (${label})`, "timeout", err);
204
217
  if (e.code === "ECONNRESET" || e.code === "ENOTFOUND" || e.code === "ECONNREFUSED")
205
- return new ProviderError(`Network error \u2014 check connection/proxy (${label}): ${e.message}`, "network", err);
206
- return new ProviderError(`Provider error (${label}): ${e.message}`, "provider", err);
218
+ return new ProviderError(
219
+ `Network error (${label}): ${e.message}
220
+ \u2192 Check your internet connection and proxy settings (HTTPS_PROXY)
221
+ \u2192 If behind a firewall, ensure the provider's API endpoint is reachable`,
222
+ "network",
223
+ err
224
+ );
225
+ const msg = e.message ?? "";
226
+ if (status === 404 || msg.includes("model") && (msg.includes("not found") || msg.includes("does not exist")))
227
+ return new ProviderError(
228
+ `Model not found (${label}): ${msg}
229
+ \u2192 Run "ai-spec model" to see available models for your provider
230
+ \u2192 The model name may have changed \u2014 check your provider's documentation`,
231
+ "provider",
232
+ err
233
+ );
234
+ if (msg.includes("insufficient") || msg.includes("quota") || msg.includes("balance"))
235
+ return new ProviderError(
236
+ `Quota/balance error (${label}): ${msg}
237
+ \u2192 Check your provider's billing dashboard
238
+ \u2192 Consider switching to a different provider with "ai-spec model"`,
239
+ "provider",
240
+ err
241
+ );
242
+ return new ProviderError(`Provider error (${label}): ${msg}`, "provider", err);
207
243
  }
208
244
  function isRetryable(err) {
209
245
  const e = err;
@@ -4765,6 +4801,21 @@ function validateDsl(raw) {
4765
4801
  for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
4766
4802
  validateEndpoint(eps[i], `endpoints[${i}]`, errors);
4767
4803
  }
4804
+ const seenEpIds = /* @__PURE__ */ new Set();
4805
+ for (let i = 0; i < Math.min(eps.length, MAX_ENDPOINTS); i++) {
4806
+ const ep = eps[i];
4807
+ if (ep && typeof ep === "object" && typeof ep["id"] === "string") {
4808
+ const id = ep["id"];
4809
+ if (seenEpIds.has(id)) {
4810
+ errors.push({
4811
+ path: `endpoints[${i}].id`,
4812
+ message: `Duplicate endpoint id "${id}" \u2014 each endpoint must have a unique id`
4813
+ });
4814
+ } else {
4815
+ seenEpIds.add(id);
4816
+ }
4817
+ }
4818
+ }
4768
4819
  }
4769
4820
  if (obj["behaviors"] !== void 0) {
4770
4821
  if (!Array.isArray(obj["behaviors"])) {
@@ -4821,6 +4872,21 @@ function validateModel(raw, path10, errors) {
4821
4872
  for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4822
4873
  validateModelField(fields[j2], `${path10}.fields[${j2}]`, errors);
4823
4874
  }
4875
+ const seenFieldNames = /* @__PURE__ */ new Set();
4876
+ for (let j2 = 0; j2 < Math.min(fields.length, MAX_FIELDS_PER_MODEL); j2++) {
4877
+ const f = fields[j2];
4878
+ if (f && typeof f === "object" && typeof f["name"] === "string") {
4879
+ const name = f["name"];
4880
+ if (seenFieldNames.has(name)) {
4881
+ errors.push({
4882
+ path: `${path10}.fields[${j2}].name`,
4883
+ message: `Duplicate field name "${name}" \u2014 each field within a model must have a unique name`
4884
+ });
4885
+ } else {
4886
+ seenFieldNames.add(name);
4887
+ }
4888
+ }
4889
+ }
4824
4890
  }
4825
4891
  if (m["relations"] !== void 0) {
4826
4892
  if (!Array.isArray(m["relations"])) {
@@ -5851,9 +5917,10 @@ ${tasks.map((t) => `${t.id} [${t.layer}] ${t.title}
5851
5917
  console.log(import_chalk8.default.cyan(` \u{1F916} Auto mode: running claude -p (non-interactive)...`));
5852
5918
  console.log(import_chalk8.default.gray(` Spec: ${specFilePath}`));
5853
5919
  try {
5854
- (0, import_child_process.execSync)(`${claudeCmd} -p "${promptContent.replace(/"/g, '\\"')}"`, {
5920
+ (0, import_child_process.spawnSync)(claudeCmd, ["-p", promptContent], {
5855
5921
  cwd: workingDir,
5856
- stdio: "inherit"
5922
+ stdio: "inherit",
5923
+ shell: false
5857
5924
  });
5858
5925
  console.log(import_chalk8.default.green("\n \u2714 Claude Code completed."));
5859
5926
  } catch {
@@ -5905,9 +5972,10 @@ Full spec is at: ${specFilePath}
5905
5972
  Implement ONLY this task. Do not implement other tasks.`;
5906
5973
  let taskStatus = "done";
5907
5974
  try {
5908
- (0, import_child_process.execSync)(`${claudeCmd} -p "${taskPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`, {
5975
+ (0, import_child_process.spawnSync)(claudeCmd, ["-p", taskPrompt], {
5909
5976
  cwd: workingDir,
5910
- stdio: "inherit"
5977
+ stdio: "inherit",
5978
+ shell: false
5911
5979
  });
5912
5980
  completed++;
5913
5981
  } catch {
@@ -6909,6 +6977,7 @@ var GitWorktreeManager = class {
6909
6977
  TaskGenerator,
6910
6978
  buildTaskPrompt,
6911
6979
  createProvider,
6980
+ extractBehavioralContract,
6912
6981
  extractComplianceScore,
6913
6982
  extractMissingCount,
6914
6983
  generateSpecWithTasks,