ai-spec-dev 0.31.0 → 0.35.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/.claude/commands/add-lesson.md +34 -0
- package/.claude/commands/check-layers.md +65 -0
- package/.claude/commands/installed-deps.md +35 -0
- package/.claude/commands/recall-lessons.md +40 -0
- package/.claude/commands/scan-singletons.md +45 -0
- package/.claude/commands/verify-imports.md +48 -0
- package/.claude/settings.local.json +15 -1
- package/README.md +531 -213
- package/RELEASE_LOG.md +460 -0
- package/cli/commands/config.ts +93 -0
- package/cli/commands/create.ts +1233 -0
- package/cli/commands/dashboard.ts +62 -0
- package/cli/commands/export.ts +66 -0
- package/cli/commands/init.ts +190 -0
- package/cli/commands/learn.ts +30 -0
- package/cli/commands/logs.ts +106 -0
- package/cli/commands/mock.ts +175 -0
- package/cli/commands/model.ts +156 -0
- package/cli/commands/restore.ts +22 -0
- package/cli/commands/review.ts +63 -0
- package/cli/commands/scan.ts +99 -0
- package/cli/commands/trend.ts +36 -0
- package/cli/commands/types.ts +69 -0
- package/cli/commands/update.ts +178 -0
- package/cli/commands/vcr.ts +70 -0
- package/cli/commands/workspace.ts +219 -0
- package/cli/index.ts +34 -2240
- package/cli/utils.ts +83 -0
- package/core/combined-generator.ts +13 -3
- package/core/dashboard-generator.ts +340 -0
- package/core/design-dialogue.ts +124 -0
- package/core/dsl-feedback.ts +285 -0
- package/core/error-feedback.ts +46 -2
- package/core/project-index.ts +301 -0
- package/core/reviewer.ts +84 -6
- package/core/run-logger.ts +109 -3
- package/core/run-trend.ts +261 -0
- package/core/self-evaluator.ts +139 -7
- package/core/spec-generator.ts +14 -8
- package/core/task-generator.ts +17 -0
- package/core/types-generator.ts +219 -0
- package/core/vcr.ts +210 -0
- package/dist/cli/index.js +6692 -4512
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +6692 -4512
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +420 -224
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +418 -224
- package/dist/index.mjs.map +1 -1
- package/docs-assets/purpose/architecture-overview.svg +64 -0
- package/docs-assets/purpose/create-pipeline.svg +113 -0
- package/docs-assets/purpose/task-layering.svg +74 -0
- package/package.json +6 -3
- package/prompts/codegen.prompt.ts +97 -9
- package/prompts/design.prompt.ts +59 -0
- package/prompts/spec.prompt.ts +8 -1
- package/prompts/tasks.prompt.ts +27 -2
- package/purpose.md +600 -174
- package/tests/dsl-extractor.test.ts +264 -0
- package/tests/dsl-feedback.test.ts +266 -0
- package/tests/dsl-validator.test.ts +283 -0
- package/tests/error-feedback.test.ts +292 -0
- package/tests/provider-utils.test.ts +173 -0
- package/tests/run-trend.test.ts +186 -0
- package/tests/self-evaluator.test.ts +339 -0
- package/tests/spec-assessor.test.ts +142 -0
- package/tests/task-generator.test.ts +230 -0
package/core/vcr.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vcr.ts — Pipeline response recording & replay for zero-cost harness iteration.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Claude Code's VCR pattern for token counting tests.
|
|
5
|
+
*
|
|
6
|
+
* Design:
|
|
7
|
+
* - VcrRecordingProvider wraps any AIProvider and intercepts every generate()
|
|
8
|
+
* call, capturing (prompt, systemInstruction, response) in order.
|
|
9
|
+
* - VcrReplayProvider implements AIProvider by returning pre-recorded responses
|
|
10
|
+
* in sequence — zero API calls, zero tokens, deterministic output.
|
|
11
|
+
* - Recordings are stored in .ai-spec-vcr/{runId}.json alongside RunLogs.
|
|
12
|
+
*
|
|
13
|
+
* Use cases:
|
|
14
|
+
* - Iterating on harness scoring weights without burning tokens
|
|
15
|
+
* - Testing prompt format changes against known pipelines
|
|
16
|
+
* - Debugging pipeline stage logic offline
|
|
17
|
+
*
|
|
18
|
+
* CLI:
|
|
19
|
+
* ai-spec create --vcr-record → record this run
|
|
20
|
+
* ai-spec create --vcr-replay <runId> → replay with zero API calls
|
|
21
|
+
* ai-spec vcr list → list available recordings
|
|
22
|
+
* ai-spec vcr show <runId> → inspect call details
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createHash } from "crypto";
|
|
26
|
+
import * as fs from "fs-extra";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
import { AIProvider } from "./spec-generator";
|
|
29
|
+
|
|
30
|
+
export const VCR_DIR = ".ai-spec-vcr";
|
|
31
|
+
|
|
32
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export interface VcrEntry {
|
|
35
|
+
/** Sequential call index within this recording */
|
|
36
|
+
index: number;
|
|
37
|
+
/** First 200 chars of prompt — for human inspection only */
|
|
38
|
+
promptPreview: string;
|
|
39
|
+
/** SHA-256[:8] of (prompt + "\x00" + systemInstruction) — stable identity */
|
|
40
|
+
callHash: string;
|
|
41
|
+
systemInstruction?: string;
|
|
42
|
+
/** Complete AI response — what replay will return */
|
|
43
|
+
response: string;
|
|
44
|
+
providerName: string;
|
|
45
|
+
modelName: string;
|
|
46
|
+
ts: string;
|
|
47
|
+
durationMs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface VcrRecording {
|
|
51
|
+
runId: string;
|
|
52
|
+
recordedAt: string;
|
|
53
|
+
/** Total number of AI calls captured */
|
|
54
|
+
entryCount: number;
|
|
55
|
+
/** Unique provider/model strings seen across all calls */
|
|
56
|
+
providers: string[];
|
|
57
|
+
entries: VcrEntry[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Recording Provider ───────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wraps a real AIProvider, transparently passing through all calls while
|
|
64
|
+
* recording each (prompt, response) pair in order.
|
|
65
|
+
* After the pipeline completes, call `save()` to persist the recording.
|
|
66
|
+
*/
|
|
67
|
+
export class VcrRecordingProvider implements AIProvider {
|
|
68
|
+
private entries: VcrEntry[] = [];
|
|
69
|
+
|
|
70
|
+
constructor(private readonly inner: AIProvider) {}
|
|
71
|
+
|
|
72
|
+
get providerName() { return this.inner.providerName; }
|
|
73
|
+
get modelName() { return this.inner.modelName; }
|
|
74
|
+
|
|
75
|
+
async generate(prompt: string, systemInstruction?: string): Promise<string> {
|
|
76
|
+
const start = Date.now();
|
|
77
|
+
const response = await this.inner.generate(prompt, systemInstruction);
|
|
78
|
+
const callHash = createHash("sha256")
|
|
79
|
+
.update(prompt + "\x00" + (systemInstruction ?? ""))
|
|
80
|
+
.digest("hex")
|
|
81
|
+
.slice(0, 8);
|
|
82
|
+
this.entries.push({
|
|
83
|
+
index: this.entries.length,
|
|
84
|
+
promptPreview: prompt.slice(0, 200).replace(/\n/g, " "),
|
|
85
|
+
callHash,
|
|
86
|
+
...(systemInstruction ? { systemInstruction } : {}),
|
|
87
|
+
response,
|
|
88
|
+
providerName: this.inner.providerName,
|
|
89
|
+
modelName: this.inner.modelName,
|
|
90
|
+
ts: new Date().toISOString(),
|
|
91
|
+
durationMs: Date.now() - start,
|
|
92
|
+
});
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get callCount() { return this.entries.length; }
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Persist the recording to .ai-spec-vcr/{runId}.json.
|
|
100
|
+
* Merges entries from an optional second recorder (e.g. codegenProvider),
|
|
101
|
+
* sorted by timestamp so replay order matches real execution order.
|
|
102
|
+
*/
|
|
103
|
+
async save(
|
|
104
|
+
workingDir: string,
|
|
105
|
+
runId: string,
|
|
106
|
+
secondRecorder?: VcrRecordingProvider
|
|
107
|
+
): Promise<string> {
|
|
108
|
+
const allEntries = secondRecorder
|
|
109
|
+
? [...this.entries, ...secondRecorder.entries].sort((a, b) => a.ts.localeCompare(b.ts))
|
|
110
|
+
: this.entries;
|
|
111
|
+
|
|
112
|
+
// Re-index after merge
|
|
113
|
+
allEntries.forEach((e, i) => { e.index = i; });
|
|
114
|
+
|
|
115
|
+
const recording: VcrRecording = {
|
|
116
|
+
runId,
|
|
117
|
+
recordedAt: new Date().toISOString(),
|
|
118
|
+
entryCount: allEntries.length,
|
|
119
|
+
providers: [...new Set(allEntries.map((e) => `${e.providerName}/${e.modelName}`))],
|
|
120
|
+
entries: allEntries,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const vcrDir = path.join(workingDir, VCR_DIR);
|
|
124
|
+
await fs.ensureDir(vcrDir);
|
|
125
|
+
const filePath = path.join(vcrDir, `${runId}.json`);
|
|
126
|
+
await fs.writeJson(filePath, recording, { spaces: 2 });
|
|
127
|
+
return filePath;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Replay Provider ──────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Implements AIProvider by replaying pre-recorded responses in sequence.
|
|
135
|
+
* Every generate() call pops the next entry from the recording — no API call,
|
|
136
|
+
* no tokens, deterministic output.
|
|
137
|
+
*
|
|
138
|
+
* Note: responses are returned in strict index order, regardless of the prompt
|
|
139
|
+
* content. This works correctly as long as the pipeline makes calls in the same
|
|
140
|
+
* structural order as the recording.
|
|
141
|
+
*/
|
|
142
|
+
export class VcrReplayProvider implements AIProvider {
|
|
143
|
+
private index = 0;
|
|
144
|
+
|
|
145
|
+
constructor(private readonly recording: VcrRecording) {}
|
|
146
|
+
|
|
147
|
+
get providerName() { return "vcr-replay"; }
|
|
148
|
+
get modelName() { return this.recording.runId; }
|
|
149
|
+
|
|
150
|
+
async generate(_prompt: string, _systemInstruction?: string): Promise<string> {
|
|
151
|
+
const entry = this.recording.entries[this.index++];
|
|
152
|
+
if (!entry) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`VCR replay exhausted: all ${this.recording.entries.length} recorded ` +
|
|
155
|
+
`responses have been consumed. The pipeline made more AI calls than the recording has.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return entry.response;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get remaining() { return this.recording.entries.length - this.index; }
|
|
162
|
+
get consumed() { return this.index; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Loader helpers ───────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
export async function loadVcrRecording(
|
|
168
|
+
workingDir: string,
|
|
169
|
+
runId: string
|
|
170
|
+
): Promise<VcrRecording | null> {
|
|
171
|
+
const filePath = path.join(workingDir, VCR_DIR, `${runId}.json`);
|
|
172
|
+
try {
|
|
173
|
+
return await fs.readJson(filePath);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface VcrSummary {
|
|
180
|
+
runId: string;
|
|
181
|
+
recordedAt: string;
|
|
182
|
+
entryCount: number;
|
|
183
|
+
providers: string[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function listVcrRecordings(workingDir: string): Promise<VcrSummary[]> {
|
|
187
|
+
const vcrDir = path.join(workingDir, VCR_DIR);
|
|
188
|
+
if (!(await fs.pathExists(vcrDir))) return [];
|
|
189
|
+
|
|
190
|
+
const files = (await fs.readdir(vcrDir))
|
|
191
|
+
.filter((f) => f.endsWith(".json"))
|
|
192
|
+
.sort()
|
|
193
|
+
.reverse();
|
|
194
|
+
|
|
195
|
+
const results: VcrSummary[] = [];
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
try {
|
|
198
|
+
const rec: VcrRecording = await fs.readJson(path.join(vcrDir, file));
|
|
199
|
+
results.push({
|
|
200
|
+
runId: rec.runId,
|
|
201
|
+
recordedAt: rec.recordedAt,
|
|
202
|
+
entryCount: rec.entryCount,
|
|
203
|
+
providers: rec.providers,
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
// skip corrupt files
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return results;
|
|
210
|
+
}
|