ai-spec-dev 0.37.0 → 0.41.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 (67) hide show
  1. package/README.md +381 -1796
  2. package/RELEASE_LOG.md +231 -0
  3. package/cli/commands/create.ts +9 -1176
  4. package/cli/commands/dashboard.ts +1 -1
  5. package/cli/pipeline/helpers.ts +34 -0
  6. package/cli/pipeline/multi-repo.ts +483 -0
  7. package/cli/pipeline/single-repo.ts +755 -0
  8. package/cli/utils.ts +2 -0
  9. package/core/code-generator.ts +52 -341
  10. package/core/codegen/helpers.ts +219 -0
  11. package/core/codegen/topo-sort.ts +98 -0
  12. package/core/constitution-consolidator.ts +2 -2
  13. package/core/dsl-coverage-checker.ts +298 -0
  14. package/core/dsl-extractor.ts +19 -46
  15. package/core/dsl-feedback.ts +1 -1
  16. package/core/dsl-validator.ts +74 -0
  17. package/core/error-feedback.ts +95 -11
  18. package/core/frontend-context-loader.ts +27 -5
  19. package/core/knowledge-memory.ts +52 -0
  20. package/core/mock/fixtures.ts +89 -0
  21. package/core/mock/proxy.ts +380 -0
  22. package/core/mock-server-generator.ts +12 -460
  23. package/core/requirement-decomposer.ts +4 -28
  24. package/core/reviewer.ts +1 -1
  25. package/core/safe-json.ts +76 -0
  26. package/core/spec-updater.ts +5 -21
  27. package/core/token-budget.ts +124 -0
  28. package/core/vcr.ts +20 -1
  29. package/dist/cli/index.js +4110 -3534
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/cli/index.mjs +4237 -3661
  32. package/dist/cli/index.mjs.map +1 -1
  33. package/dist/index.d.mts +18 -16
  34. package/dist/index.d.ts +18 -16
  35. package/dist/index.js +310 -182
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +308 -180
  38. package/dist/index.mjs.map +1 -1
  39. package/package.json +2 -2
  40. package/purpose.md +173 -33
  41. package/tests/auto-consolidation.test.ts +109 -0
  42. package/tests/combined-generator.test.ts +81 -0
  43. package/tests/constitution-consolidator.test.ts +161 -0
  44. package/tests/constitution-generator.test.ts +94 -0
  45. package/tests/contract-bridge.test.ts +201 -0
  46. package/tests/design-dialogue.test.ts +108 -0
  47. package/tests/dsl-coverage-checker.test.ts +230 -0
  48. package/tests/dsl-feedback.test.ts +45 -0
  49. package/tests/dsl-validator-xref.test.ts +99 -0
  50. package/tests/error-feedback-repair.test.ts +319 -0
  51. package/tests/error-feedback-validation.test.ts +91 -0
  52. package/tests/frontend-context-loader.test.ts +609 -0
  53. package/tests/global-constitution.test.ts +110 -0
  54. package/tests/key-store.test.ts +73 -0
  55. package/tests/knowledge-memory.test.ts +327 -0
  56. package/tests/project-index.test.ts +206 -0
  57. package/tests/prompt-hasher.test.ts +19 -0
  58. package/tests/requirement-decomposer.test.ts +171 -0
  59. package/tests/reviewer.test.ts +4 -1
  60. package/tests/run-logger.test.ts +289 -0
  61. package/tests/run-snapshot.test.ts +113 -0
  62. package/tests/safe-json.test.ts +63 -0
  63. package/tests/spec-updater.test.ts +161 -0
  64. package/tests/test-generator.test.ts +146 -0
  65. package/tests/token-budget.test.ts +124 -0
  66. package/tests/vcr-hash.test.ts +101 -0
  67. package/tests/workspace-loader.test.ts +277 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * token-budget.ts — Lightweight token estimation and priority-based context assembly.
3
+ *
4
+ * Prevents silent context window overflow by estimating tokens and
5
+ * trimming lower-priority sections when the budget is exceeded.
6
+ */
7
+
8
+ import chalk from "chalk";
9
+
10
+ // ─── Token Estimation ────────────────────────────────────────────────────────
11
+
12
+ /** CJK character range. */
13
+ const CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
14
+
15
+ /**
16
+ * Estimate token count for a string.
17
+ * CJK characters ≈ 1 token each; English/code ≈ 1 token per 4 characters.
18
+ * This is deliberately conservative (over-estimates slightly) to avoid
19
+ * exceeding the actual context window.
20
+ */
21
+ export function estimateTokens(text: string): number {
22
+ if (!text) return 0;
23
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
24
+ const nonCjkLength = text.length - cjkCount;
25
+ return Math.ceil(cjkCount + nonCjkLength / 4);
26
+ }
27
+
28
+ // ─── Budget Assembly ────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * A named section of context with a priority level.
32
+ * Lower priority number = higher importance (trimmed last).
33
+ */
34
+ export interface BudgetSection {
35
+ /** Section name for logging. */
36
+ name: string;
37
+ /** The actual text content. */
38
+ content: string;
39
+ /** Priority: 1 = highest (never trim), 5 = lowest (trim first). */
40
+ priority: number;
41
+ }
42
+
43
+ export interface BudgetResult {
44
+ /** The assembled prompt text (all included sections concatenated). */
45
+ assembledPrompt: string;
46
+ /** Total estimated tokens in the assembled prompt. */
47
+ totalTokens: number;
48
+ /** Names of sections that were trimmed or dropped. */
49
+ trimmedSections: string[];
50
+ }
51
+
52
+ /**
53
+ * Assemble context sections within a token budget.
54
+ *
55
+ * Sections are added in priority order (P1 first). When the budget is
56
+ * exceeded, lower-priority sections are truncated or dropped entirely.
57
+ *
58
+ * @param sections - Context sections to assemble.
59
+ * @param maxTokens - Maximum token budget.
60
+ */
61
+ export function assembleSections(
62
+ sections: BudgetSection[],
63
+ maxTokens: number
64
+ ): BudgetResult {
65
+ // Sort by priority (ascending = most important first)
66
+ const sorted = [...sections].sort((a, b) => a.priority - b.priority);
67
+
68
+ const included: string[] = [];
69
+ const trimmedSections: string[] = [];
70
+ let usedTokens = 0;
71
+
72
+ for (const section of sorted) {
73
+ if (!section.content) continue;
74
+
75
+ const sectionTokens = estimateTokens(section.content);
76
+
77
+ if (usedTokens + sectionTokens <= maxTokens) {
78
+ // Fits entirely
79
+ included.push(section.content);
80
+ usedTokens += sectionTokens;
81
+ } else {
82
+ const remainingBudget = maxTokens - usedTokens;
83
+ if (remainingBudget <= 100) {
84
+ // Not enough room even for a truncated version
85
+ trimmedSections.push(section.name);
86
+ continue;
87
+ }
88
+
89
+ // Truncate to fit remaining budget (approximate: 4 chars per token for safety)
90
+ const charBudget = Math.floor(remainingBudget * 3);
91
+ const truncated = section.content.slice(0, charBudget);
92
+ included.push(truncated + `\n\n... [${section.name} truncated — context budget reached]`);
93
+ trimmedSections.push(section.name);
94
+ usedTokens = maxTokens; // budget exhausted
95
+ }
96
+ }
97
+
98
+ const assembledPrompt = included.join("\n\n");
99
+
100
+ if (trimmedSections.length > 0) {
101
+ console.log(
102
+ chalk.yellow(
103
+ ` ⚠ Token budget: ${usedTokens}/${maxTokens} tokens. Trimmed: ${trimmedSections.join(", ")}`
104
+ )
105
+ );
106
+ }
107
+
108
+ return { assembledPrompt, totalTokens: usedTokens, trimmedSections };
109
+ }
110
+
111
+ // ─── Default Budgets ─────────────────────────────────────────────────────────
112
+
113
+ /** Default context token budgets per provider. */
114
+ export const DEFAULT_TOKEN_BUDGETS: Record<string, number> = {
115
+ gemini: 900_000,
116
+ claude: 180_000,
117
+ openai: 120_000,
118
+ deepseek: 60_000,
119
+ default: 100_000,
120
+ };
121
+
122
+ export function getDefaultBudget(providerName: string): number {
123
+ return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
124
+ }
package/core/vcr.ts CHANGED
@@ -141,13 +141,14 @@ export class VcrRecordingProvider implements AIProvider {
141
141
  */
142
142
  export class VcrReplayProvider implements AIProvider {
143
143
  private index = 0;
144
+ private _mismatches: Array<{ index: number; expected: string; actual: string }> = [];
144
145
 
145
146
  constructor(private readonly recording: VcrRecording) {}
146
147
 
147
148
  get providerName() { return "vcr-replay"; }
148
149
  get modelName() { return this.recording.runId; }
149
150
 
150
- async generate(_prompt: string, _systemInstruction?: string): Promise<string> {
151
+ async generate(prompt: string, systemInstruction?: string): Promise<string> {
151
152
  const entry = this.recording.entries[this.index++];
152
153
  if (!entry) {
153
154
  throw new Error(
@@ -155,11 +156,29 @@ export class VcrReplayProvider implements AIProvider {
155
156
  `responses have been consumed. The pipeline made more AI calls than the recording has.`
156
157
  );
157
158
  }
159
+
160
+ // Validate prompt hash to detect pipeline drift
161
+ const actualHash = createHash("sha256")
162
+ .update(prompt + "\x00" + (systemInstruction ?? ""))
163
+ .digest("hex")
164
+ .slice(0, 8);
165
+ if (actualHash !== entry.callHash) {
166
+ this._mismatches.push({
167
+ index: entry.index,
168
+ expected: entry.callHash,
169
+ actual: actualHash,
170
+ });
171
+ }
172
+
158
173
  return entry.response;
159
174
  }
160
175
 
161
176
  get remaining() { return this.recording.entries.length - this.index; }
162
177
  get consumed() { return this.index; }
178
+
179
+ /** Returns prompt hash mismatches detected during replay. */
180
+ get mismatches() { return this._mismatches; }
181
+ get hasMismatches() { return this._mismatches.length > 0; }
163
182
  }
164
183
 
165
184
  // ─── Loader helpers ───────────────────────────────────────────────────────────