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.
Files changed (70) hide show
  1. package/README.md +86 -40
  2. package/cli/commands/config.ts +129 -1
  3. package/cli/commands/create.ts +246 -11
  4. package/cli/commands/fix-history.ts +176 -0
  5. package/cli/commands/init.ts +344 -106
  6. package/cli/index.ts +3 -7
  7. package/cli/pipeline/helpers.ts +6 -0
  8. package/cli/pipeline/multi-repo.ts +291 -26
  9. package/cli/pipeline/single-repo.ts +103 -2
  10. package/cli/utils.ts +95 -4
  11. package/core/code-generator.ts +63 -14
  12. package/core/config-defaults.ts +44 -0
  13. package/core/constitution-generator.ts +2 -1
  14. package/core/cross-stack-verifier.ts +395 -0
  15. package/core/dsl-extractor.ts +2 -1
  16. package/core/error-feedback.ts +3 -2
  17. package/core/fix-history.ts +333 -0
  18. package/core/import-fixer.ts +827 -0
  19. package/core/import-verifier.ts +569 -0
  20. package/core/knowledge-memory.ts +55 -6
  21. package/core/openapi-exporter.ts +3 -2
  22. package/core/repo-store.ts +95 -0
  23. package/core/reviewer.ts +14 -13
  24. package/core/run-logger.ts +3 -4
  25. package/core/run-snapshot.ts +2 -3
  26. package/core/run-trend.ts +3 -4
  27. package/core/self-evaluator.ts +44 -7
  28. package/core/spec-generator.ts +30 -45
  29. package/core/token-budget.ts +3 -8
  30. package/core/types-generator.ts +2 -2
  31. package/core/vcr.ts +3 -1
  32. package/dist/cli/index.js +3889 -1937
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/index.mjs +3888 -1936
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/index.d.mts +17 -2
  37. package/dist/index.d.ts +17 -2
  38. package/dist/index.js +292 -181
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +292 -181
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +2 -2
  43. package/tests/cross-stack-verifier.test.ts +301 -0
  44. package/tests/fix-history.test.ts +335 -0
  45. package/tests/import-fixer.test.ts +944 -0
  46. package/tests/import-verifier.test.ts +420 -0
  47. package/tests/knowledge-memory.test.ts +40 -0
  48. package/tests/self-evaluator.test.ts +97 -0
  49. package/cli/commands/model.ts +0 -156
  50. package/cli/commands/scan.ts +0 -99
  51. package/cli/commands/workspace.ts +0 -219
  52. package/demo-backend/.ai-spec-constitution.md +0 -65
  53. package/demo-backend/package.json +0 -21
  54. package/demo-backend/prisma/schema.prisma +0 -22
  55. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
  56. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
  57. package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
  58. package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
  59. package/demo-backend/src/index.ts +0 -17
  60. package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
  61. package/demo-backend/src/routes/bookmark.routes.ts +0 -11
  62. package/demo-backend/src/routes/index.ts +0 -8
  63. package/demo-backend/src/services/bookmark.service.test.ts +0 -433
  64. package/demo-backend/src/services/bookmark.service.ts +0 -261
  65. package/demo-backend/tsconfig.json +0 -12
  66. package/demo-frontend/.ai-spec-constitution.md +0 -95
  67. package/demo-frontend/package.json +0 -23
  68. package/demo-frontend/src/App.tsx +0 -12
  69. package/demo-frontend/src/main.tsx +0 -9
  70. package/demo-frontend/tsconfig.json +0 -13
package/cli/utils.ts CHANGED
@@ -1,37 +1,120 @@
1
1
  import * as path from "path";
2
2
  import * as fs from "fs-extra";
3
+ import * as os from "os";
3
4
  import chalk from "chalk";
4
5
  import { input, select } from "@inquirer/prompts";
5
6
  import { CodeGenMode } from "../core/code-generator";
6
- import { ENV_KEY_MAP } from "../core/spec-generator";
7
+ import { ENV_KEY_MAP, PROVIDER_CATALOG } from "../core/spec-generator";
7
8
  import { getSavedKey, saveKey, KEY_STORE_FILE } from "../core/key-store";
8
9
 
9
10
  // ─── Config ───────────────────────────────────────────────────────────────────
10
11
 
11
- export interface AiSpecConfig {
12
+ /** User-level preferences (stored in ~/.ai-spec-config.json) */
13
+ export interface AiSpecGlobalConfig {
12
14
  provider?: string;
13
15
  model?: string;
14
16
  codegen?: CodeGenMode;
15
17
  codegenProvider?: string;
16
18
  codegenModel?: string;
19
+ }
20
+
21
+ /** Full merged config (global + project-level overrides) */
22
+ export interface AiSpecConfig extends AiSpecGlobalConfig {
17
23
  /** Minimum overall spec score (1-10) required to pass Approval Gate. 0 = disabled (default). */
18
24
  minSpecScore?: number;
19
25
  /** Minimum harness score (1-10) required for pipeline success. 0 = disabled (default). */
20
26
  minHarnessScore?: number;
21
27
  /** Maximum error-feedback cycles before giving up (default: 2, TDD default: 3). */
22
28
  maxErrorCycles?: number;
29
+ /**
30
+ * Maximum number of tasks that can run concurrently within a codegen batch
31
+ * (api mode only). Prevents rate-limit errors when a layer has many independent
32
+ * tasks. Default: 3.
33
+ */
34
+ maxCodegenConcurrency?: number;
35
+ /**
36
+ * When true (default), past hallucination patterns from
37
+ * `.ai-spec-fix-history.json` are injected into codegen prompts.
38
+ * Set to false to disable automatic learning from fix history.
39
+ */
40
+ injectFixHistory?: boolean;
41
+ /**
42
+ * Number of times a hallucination pattern must repeat in fix-history before
43
+ * `ai-spec fix-history --promote` offers it as a constitution §9 lesson.
44
+ * Default: 5.
45
+ */
46
+ fixHistoryPromotionThreshold?: number;
47
+ /**
48
+ * Maximum number of past hallucination patterns injected into a single
49
+ * codegen prompt. Prevents prompt bloat. Default: 10.
50
+ */
51
+ fixHistoryInjectMax?: number;
23
52
  /** §9 lesson count threshold for auto-consolidation (default: 12). */
24
53
  autoConsolidateThreshold?: number;
54
+
55
+ // ── Directory & file overrides ─────────────────────────────────────────────
56
+ /** Run log directory (default: ".ai-spec-logs") */
57
+ logDir?: string;
58
+ /** VCR recording directory (default: ".ai-spec-vcr") */
59
+ vcrDir?: string;
60
+ /** File backup directory (default: ".ai-spec-backup") */
61
+ backupDir?: string;
62
+ /** Review history file (default: ".ai-spec-reviews.json") */
63
+ reviewHistoryFile?: string;
64
+
65
+ // ── URL overrides ──────────────────────────────────────────────────────────
66
+ /** Default server URL for OpenAPI export (default: "http://localhost:3000") */
67
+ openApiServerUrl?: string;
68
+
69
+ // ── Numeric limits ─────────────────────────────────────────────────────────
70
+ /** Max chars captured from build/test/lint command output (default: 30000) */
71
+ maxCommandOutputChars?: number;
72
+ /** Max chars of source file sent to AI for auto-fix (default: 60000) */
73
+ maxFixFileChars?: number;
74
+ /** Max DSL extraction retries (default: 2) */
75
+ dslMaxRetries?: number;
76
+ /** Max constitution chars in codegen prompt (default: 4000) */
77
+ maxConstitutionChars?: number;
78
+ /** Per-provider token budget overrides (e.g. { "gemini": 900000, "claude": 180000 }) */
79
+ providerTokenBudgets?: Record<string, number>;
25
80
  }
26
81
 
27
82
  export const CONFIG_FILE = ".ai-spec.json";
83
+ export const GLOBAL_CONFIG_FILE = path.join(os.homedir(), ".ai-spec-config.json");
84
+
85
+ /** Load global user-level config from ~/.ai-spec-config.json */
86
+ export async function loadGlobalConfig(): Promise<AiSpecGlobalConfig> {
87
+ try {
88
+ if (await fs.pathExists(GLOBAL_CONFIG_FILE)) {
89
+ return await fs.readJson(GLOBAL_CONFIG_FILE);
90
+ }
91
+ } catch { /* ignore */ }
92
+ return {};
93
+ }
28
94
 
95
+ /** Save global user-level config to ~/.ai-spec-config.json */
96
+ export async function saveGlobalConfig(config: AiSpecGlobalConfig): Promise<void> {
97
+ await fs.ensureFile(GLOBAL_CONFIG_FILE);
98
+ await fs.writeJson(GLOBAL_CONFIG_FILE, config, { spaces: 2 });
99
+ }
100
+
101
+ /**
102
+ * Load merged config: global (baseline) + project-level (override).
103
+ * Provider/model from global, project-specific settings from local .ai-spec.json.
104
+ */
29
105
  export async function loadConfig(dir: string): Promise<AiSpecConfig> {
106
+ const globalConfig = await loadGlobalConfig();
107
+
108
+ let localConfig: AiSpecConfig = {};
30
109
  const p = path.join(dir, CONFIG_FILE);
31
110
  if (await fs.pathExists(p)) {
32
- return fs.readJson(p);
111
+ try {
112
+ localConfig = await fs.readJson(p);
113
+ } catch { /* ignore */ }
33
114
  }
34
- return {};
115
+
116
+ // Local overrides global
117
+ return { ...globalConfig, ...localConfig };
35
118
  }
36
119
 
37
120
  // ─── API Key Resolution ───────────────────────────────────────────────────────
@@ -45,6 +128,14 @@ export async function resolveApiKey(
45
128
  const envVar = ENV_KEY_MAP[providerName];
46
129
  if (envVar && process.env[envVar]) return process.env[envVar]!;
47
130
 
131
+ // Check fallback env vars (e.g. MiMo reads ANTHROPIC_AUTH_TOKEN from token-plan)
132
+ const meta = PROVIDER_CATALOG[providerName];
133
+ if (meta?.fallbackEnvKeys) {
134
+ for (const key of meta.fallbackEnvKeys) {
135
+ if (process.env[key]) return process.env[key]!;
136
+ }
137
+ }
138
+
48
139
  const savedKey = await getSavedKey(providerName);
49
140
  if (savedKey) {
50
141
  const masked = savedKey.slice(0, 6) + "..." + savedKey.slice(-4);
@@ -23,6 +23,7 @@ import {
23
23
  import { topoSortLayerTasks, printTaskProgress, LAYER_ICONS } from "./codegen/topo-sort";
24
24
  import { estimateTokens, getDefaultBudget } from "./token-budget";
25
25
  import { startSpinner } from "./cli-ui";
26
+ import { loadFixHistory, buildHallucinationAvoidanceSection } from "./fix-history";
26
27
 
27
28
  // Re-export public symbols for backward compatibility
28
29
  export { extractBehavioralContract } from "./codegen/helpers";
@@ -41,6 +42,20 @@ export interface CodeGenOptions {
41
42
  dslFilePath?: string;
42
43
  /** Repo language type — selects the appropriate codegen system prompt */
43
44
  repoType?: string;
45
+ /**
46
+ * Maximum number of tasks that can run concurrently within a single batch
47
+ * (api mode only). A batch larger than this value is split into sequential
48
+ * sub-chunks, each running maxConcurrency tasks in parallel. Default: 3.
49
+ */
50
+ maxConcurrency?: number;
51
+ /**
52
+ * When true, prior hallucination patterns from `.ai-spec-fix-history.json`
53
+ * are injected into the codegen prompt as a "DO NOT REPEAT" section.
54
+ * Default: true when the ledger exists.
55
+ */
56
+ injectFixHistory?: boolean;
57
+ /** Max number of past hallucination patterns to inject. Default: 10 */
58
+ fixHistoryInjectMax?: number;
44
59
  }
45
60
 
46
61
  export class CodeGenerator {
@@ -268,8 +283,27 @@ export class CodeGenerator {
268
283
  console.log(chalk.gray(` Frontend context: ${fctx.framework} / ${fctx.httpClient} | hooks:${fctx.hookFiles.length} stores:${fctx.storeFiles.length}`));
269
284
  }
270
285
 
286
+ // Inject past hallucination patterns so the AI learns from this project's fix history.
287
+ // Opt out via CodeGenOptions.injectFixHistory = false.
288
+ let fixHistorySection = "";
289
+ if (options.injectFixHistory !== false) {
290
+ try {
291
+ const history = await loadFixHistory(workingDir);
292
+ const section = buildHallucinationAvoidanceSection(history, {
293
+ maxItems: options.fixHistoryInjectMax ?? 10,
294
+ });
295
+ if (section) {
296
+ fixHistorySection = `\n${section}\n`;
297
+ const patternCount = (section.match(/❌ Do NOT/g) ?? []).length;
298
+ console.log(chalk.cyan(` ✓ Injected ${patternCount} prior hallucination pattern(s) from fix-history`));
299
+ }
300
+ } catch {
301
+ // Non-fatal: if the ledger is broken, just skip injection
302
+ }
303
+ }
304
+
271
305
  // Token budget check — warn if context sections are large
272
- const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection;
306
+ const allContextText = spec + constitutionSection + dslSection + frontendSection + installedPackagesSection + sharedConfigSection + fixHistorySection;
273
307
  const estimatedTokenCount = estimateTokens(allContextText);
274
308
  const budget = getDefaultBudget(this.provider.providerName);
275
309
  if (estimatedTokenCount > budget * 0.7) {
@@ -292,7 +326,7 @@ export class CodeGenerator {
292
326
  // Use tasks if available for finer-grained generation with resume support
293
327
  const tasks = await loadTasksForSpec(specFilePath);
294
328
  if (tasks && tasks.length > 0) {
295
- return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection, frontendSection, sharedConfigSection, options, systemPrompt, context);
329
+ return this.runApiModeWithTasks(spec, tasks, specFilePath, workingDir, constitutionSection + dslSection + installedPackagesSection + fixHistorySection, frontendSection, sharedConfigSection, options, systemPrompt, context);
296
330
  }
297
331
 
298
332
  // Fallback: plan-then-generate
@@ -306,7 +340,7 @@ IMPORTANT: Check the "Frontend Project Context" section below. Extend existing h
306
340
 
307
341
  === Feature Spec ===
308
342
  ${spec}
309
- ${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}
343
+ ${constitutionSection}${dslSection}${frontendSection}${installedPackagesSection}${sharedConfigSection}${fixHistorySection}
310
344
  === Project Context ===
311
345
  ${contextSummary}
312
346
 
@@ -336,7 +370,7 @@ Output ONLY a valid JSON array:
336
370
  console.log(` ${icon} ${item.file}: ${chalk.gray(item.description)}`);
337
371
  });
338
372
 
339
- const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection, systemPrompt);
373
+ const { files } = await this.generateFiles(filePlan, spec, workingDir, constitutionSection + dslSection + frontendSection + installedPackagesSection + fixHistorySection, systemPrompt);
340
374
  return files;
341
375
  }
342
376
 
@@ -524,24 +558,39 @@ Output ONLY a valid JSON array:
524
558
 
525
559
  // Partition tasks into topological batches (respects dependencies field).
526
560
  // Each batch runs in parallel; batches run sequentially.
561
+ // Additionally, each batch is chunked by maxConcurrency to prevent
562
+ // rate-limit errors when a batch contains many independent tasks.
527
563
  const taskBatches = topoSortLayerTasks(layerTasks);
528
564
  const layerResults: TaskResult[] = [];
565
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? 3);
529
566
 
530
567
  for (const batch of taskBatches) {
531
568
  const batchIsParallel = batch.length > 1;
532
- const batchResultPromises = batch.map((task) => executeTask(task, batchIsParallel));
533
- const settled = await Promise.allSettled(batchResultPromises);
534
569
  const batchResults: TaskResult[] = [];
535
- for (let i = 0; i < settled.length; i++) {
536
- const outcome = settled[i];
537
- if (outcome.status === "fulfilled") {
538
- batchResults.push(outcome.value);
539
- } else {
540
- const task = batch[i];
541
- console.log(chalk.yellow(` ⚠ ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
542
- batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
570
+
571
+ // Split batch into chunks of at most `maxConcurrency` tasks.
572
+ // Each chunk runs in parallel; chunks run sequentially within the batch.
573
+ for (let chunkStart = 0; chunkStart < batch.length; chunkStart += maxConcurrency) {
574
+ const chunk = batch.slice(chunkStart, chunkStart + maxConcurrency);
575
+ if (batchIsParallel && batch.length > maxConcurrency) {
576
+ const chunkIdx = Math.floor(chunkStart / maxConcurrency) + 1;
577
+ const totalChunks = Math.ceil(batch.length / maxConcurrency);
578
+ console.log(chalk.gray(` ↳ chunk ${chunkIdx}/${totalChunks} (${chunk.length} tasks, concurrency cap: ${maxConcurrency})`));
579
+ }
580
+ const chunkResultPromises = chunk.map((task) => executeTask(task, batchIsParallel));
581
+ const settled = await Promise.allSettled(chunkResultPromises);
582
+ for (let i = 0; i < settled.length; i++) {
583
+ const outcome = settled[i];
584
+ if (outcome.status === "fulfilled") {
585
+ batchResults.push(outcome.value);
586
+ } else {
587
+ const task = chunk[i];
588
+ console.log(chalk.yellow(` ⚠ ${task.id} threw unexpectedly: ${outcome.reason?.message ?? outcome.reason}`));
589
+ batchResults.push({ task, files: [], createdFiles: [], success: 0, total: 0, impliesRegistration: false });
590
+ }
543
591
  }
544
592
  }
593
+
545
594
  layerResults.push(...batchResults);
546
595
  // Update cache after each batch so the next batch sees the exports.
547
596
  await updateCacheFromBatch(batchResults);
@@ -0,0 +1,44 @@
1
+ /**
2
+ * config-defaults.ts — Centralized default values for all configurable constants.
3
+ *
4
+ * Modules import their defaults from here instead of defining local magic numbers.
5
+ * The pipeline can override any value via AiSpecConfig at runtime.
6
+ */
7
+
8
+ // ─── Directory & File Names ─────────────────────────────────────────────────
9
+
10
+ export const DEFAULT_LOG_DIR = ".ai-spec-logs";
11
+ export const DEFAULT_VCR_DIR = ".ai-spec-vcr";
12
+ export const DEFAULT_BACKUP_DIR = ".ai-spec-backup";
13
+ export const DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
14
+
15
+ // ─── URLs ───────────────────────────────────────────────────────────────────
16
+
17
+ export const DEFAULT_OPENAPI_SERVER_URL = "http://localhost:3000";
18
+
19
+ // ─── Numeric Limits ─────────────────────────────────────────────────────────
20
+
21
+ /** Max chars captured from build/test/lint command output before parsing. */
22
+ export const DEFAULT_MAX_COMMAND_OUTPUT_CHARS = 30_000;
23
+
24
+ /** Max chars of an existing file sent to the AI for auto-fix. */
25
+ export const DEFAULT_MAX_FIX_FILE_CHARS = 60_000;
26
+
27
+ /** Max DSL extraction retries on parse failure. */
28
+ export const DEFAULT_DSL_MAX_RETRIES = 2;
29
+
30
+ /** Max constitution chars in codegen prompts (trimmed if exceeded). */
31
+ export const DEFAULT_MAX_CONSTITUTION_CHARS = 4_000;
32
+
33
+ /** Max chars of file content sent per file in review (reviewFiles mode). */
34
+ export const DEFAULT_MAX_REVIEW_FILE_CHARS = 3_000;
35
+
36
+ // ─── Token Budgets ──────────────────────────────────────────────────────────
37
+
38
+ export const DEFAULT_TOKEN_BUDGETS: Record<string, number> = {
39
+ gemini: 900_000,
40
+ claude: 180_000,
41
+ openai: 120_000,
42
+ deepseek: 60_000,
43
+ default: 100_000,
44
+ };
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import { AIProvider } from "./spec-generator";
5
5
  import { ContextLoader, ProjectContext } from "./context-loader";
6
6
  import { constitutionSystemPrompt } from "../prompts/constitution.prompt";
7
+ import { DEFAULT_MAX_CONSTITUTION_CHARS } from "./config-defaults";
7
8
 
8
9
  export const CONSTITUTION_FILE = ".ai-spec-constitution.md";
9
10
 
@@ -41,7 +42,7 @@ function buildConstitutionPrompt(context: ProjectContext, projectRoot: string):
41
42
  }
42
43
 
43
44
  if (context.schema) {
44
- parts.push(`=== Prisma Schema ===\n${context.schema.slice(0, 4000)}\n`);
45
+ parts.push(`=== Prisma Schema ===\n${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}\n`);
45
46
  }
46
47
 
47
48
  if (context.errorPatterns) {