@stupify/cli 0.0.3 → 0.0.4
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/README.md +26 -31
- package/dist/analysis.d.ts +11 -9
- package/dist/analysis.js +30 -173
- package/dist/checks.d.ts +1 -0
- package/dist/checks.js +89 -2
- package/dist/command.js +55 -91
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/counter-scout.js +70 -8
- package/dist/doctor.d.ts +4 -0
- package/dist/doctor.js +131 -0
- package/dist/git.d.ts +4 -1
- package/dist/git.js +34 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +117 -0
- package/dist/model.d.ts +1 -15
- package/dist/model.js +37 -21
- package/dist/prompts.d.ts +8 -5
- package/dist/prompts.js +58 -168
- package/dist/render.d.ts +2 -2
- package/dist/render.js +70 -78
- package/dist/repomix-provider.d.ts +10 -2
- package/dist/repomix-provider.js +62 -11
- package/dist/search-bench.d.ts +1 -0
- package/dist/search-bench.js +675 -0
- package/dist/search-profile.d.ts +6 -0
- package/dist/search-profile.js +73 -0
- package/dist/sem-provider.d.ts +2 -2
- package/dist/sem-provider.js +33 -7
- package/dist/stupify.d.ts +2 -0
- package/dist/stupify.js +183 -333
- package/dist/types.d.ts +193 -109
- package/package.json +1 -1
- package/src/analysis.ts +48 -268
- package/src/checks.ts +91 -2
- package/src/command.ts +62 -107
- package/src/constants.ts +1 -1
- package/src/counter-scout.ts +63 -7
- package/src/doctor.ts +140 -0
- package/src/git.ts +35 -1
- package/src/hooks.ts +134 -0
- package/src/model.ts +39 -26
- package/src/prompts.ts +66 -202
- package/src/render.ts +68 -79
- package/src/repomix-provider.ts +66 -10
- package/src/search-bench.ts +783 -0
- package/src/search-profile.ts +89 -0
- package/src/sem-provider.ts +36 -9
- package/src/stupify.ts +213 -526
- package/src/types.ts +195 -119
- package/dist/batcher.d.ts +0 -3
- package/dist/batcher.js +0 -142
- package/dist/candidate-context.d.ts +0 -2
- package/dist/candidate-context.js +0 -40
- package/dist/experiment.d.ts +0 -1
- package/dist/experiment.js +0 -225
- package/src/batcher.ts +0 -198
- package/src/candidate-context.ts +0 -43
- package/src/experiment.ts +0 -317
package/src/experiment.ts
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
|
|
6
|
-
const execFileAsync = promisify(execFile);
|
|
7
|
-
|
|
8
|
-
type ExperimentConfig = Readonly<{
|
|
9
|
-
name: string;
|
|
10
|
-
cwd?: string;
|
|
11
|
-
baseCommand: readonly string[];
|
|
12
|
-
runs: readonly ExperimentRun[];
|
|
13
|
-
}>;
|
|
14
|
-
|
|
15
|
-
type ExperimentRun = Readonly<{
|
|
16
|
-
id: string;
|
|
17
|
-
args: readonly string[];
|
|
18
|
-
}>;
|
|
19
|
-
|
|
20
|
-
type ExperimentSummary = Readonly<{
|
|
21
|
-
id: string;
|
|
22
|
-
command: string;
|
|
23
|
-
totalMs: number;
|
|
24
|
-
entitiesScanned: number;
|
|
25
|
-
targets: number;
|
|
26
|
-
targetsByCheck: Record<string, number>;
|
|
27
|
-
auditContext: string;
|
|
28
|
-
auditPrompt: string;
|
|
29
|
-
repomixFiles: number;
|
|
30
|
-
repomixTokens: number;
|
|
31
|
-
auditCalls: number;
|
|
32
|
-
auditInputTokens: readonly number[];
|
|
33
|
-
auditMs: number;
|
|
34
|
-
findings: number;
|
|
35
|
-
uncertain: number;
|
|
36
|
-
clean: number;
|
|
37
|
-
findingsByCheck: Record<string, number>;
|
|
38
|
-
uncertainByCheck: Record<string, number>;
|
|
39
|
-
targetPreview: readonly DebugTargetRecord[];
|
|
40
|
-
errors: readonly string[];
|
|
41
|
-
}>;
|
|
42
|
-
|
|
43
|
-
type DebugTargetRecord = Readonly<{
|
|
44
|
-
targetId: string;
|
|
45
|
-
checkId: string;
|
|
46
|
-
entityId: string;
|
|
47
|
-
entityKind?: string;
|
|
48
|
-
changeKind?: string;
|
|
49
|
-
scoutReason?: string;
|
|
50
|
-
sourceLabel?: string;
|
|
51
|
-
}>;
|
|
52
|
-
|
|
53
|
-
export async function runExperiment(configPath: string): Promise<string> {
|
|
54
|
-
const config = await readConfig(configPath);
|
|
55
|
-
const startedAt = new Date();
|
|
56
|
-
const outputDir = path.join(
|
|
57
|
-
process.cwd(),
|
|
58
|
-
"experiments",
|
|
59
|
-
"results",
|
|
60
|
-
`${safeSegment(config.name)}-${timestamp(startedAt)}`,
|
|
61
|
-
);
|
|
62
|
-
await mkdir(outputDir, { recursive: true });
|
|
63
|
-
|
|
64
|
-
const cwd = resolveCwd(config.cwd);
|
|
65
|
-
const cliPath = path.resolve(process.argv[1] ?? "packages/cli/dist/stupify.js");
|
|
66
|
-
const summaries: ExperimentSummary[] = [];
|
|
67
|
-
|
|
68
|
-
for (const run of config.runs) {
|
|
69
|
-
const args = ensureJson([...config.baseCommand, ...run.args]);
|
|
70
|
-
const command = [process.execPath, cliPath, ...args].join(" ");
|
|
71
|
-
const summary = await runOneExperiment({
|
|
72
|
-
id: run.id,
|
|
73
|
-
args,
|
|
74
|
-
command,
|
|
75
|
-
cwd,
|
|
76
|
-
cliPath,
|
|
77
|
-
outputDir,
|
|
78
|
-
});
|
|
79
|
-
summaries.push(summary);
|
|
80
|
-
await writeFile(
|
|
81
|
-
path.join(outputDir, "summary.json"),
|
|
82
|
-
`${JSON.stringify({ name: config.name, cwd, runs: summaries }, null, 2)}\n`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return outputDir;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function readConfig(configPath: string): Promise<ExperimentConfig> {
|
|
90
|
-
const fullPath = path.resolve(configPath);
|
|
91
|
-
const raw = await readFile(fullPath, "utf8");
|
|
92
|
-
const parsed = JSON.parse(raw) as Partial<ExperimentConfig>;
|
|
93
|
-
if (!parsed.name) throw new Error("Experiment config requires name.");
|
|
94
|
-
if (!Array.isArray(parsed.baseCommand)) throw new Error("Experiment config requires baseCommand array.");
|
|
95
|
-
if (!Array.isArray(parsed.runs)) throw new Error("Experiment config requires runs array.");
|
|
96
|
-
return {
|
|
97
|
-
name: parsed.name,
|
|
98
|
-
cwd: parsed.cwd,
|
|
99
|
-
baseCommand: parsed.baseCommand,
|
|
100
|
-
runs: parsed.runs,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function runOneExperiment(input: Readonly<{
|
|
105
|
-
id: string;
|
|
106
|
-
args: readonly string[];
|
|
107
|
-
command: string;
|
|
108
|
-
cwd: string;
|
|
109
|
-
cliPath: string;
|
|
110
|
-
outputDir: string;
|
|
111
|
-
}>): Promise<ExperimentSummary> {
|
|
112
|
-
const startedAt = Date.now();
|
|
113
|
-
try {
|
|
114
|
-
const { stdout, stderr } = await execFileAsync(
|
|
115
|
-
process.execPath,
|
|
116
|
-
[input.cliPath, ...input.args],
|
|
117
|
-
{
|
|
118
|
-
cwd: input.cwd,
|
|
119
|
-
maxBuffer: 128 * 1024 * 1024,
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
if (stderr.trim()) {
|
|
123
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stderr.txt`), stderr);
|
|
124
|
-
}
|
|
125
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.json`), stdout);
|
|
126
|
-
const report = JSON.parse(stdout) as Record<string, unknown>;
|
|
127
|
-
const summary = summarizeReport(input.id, input.command, Date.now() - startedAt, report, []);
|
|
128
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}-findings.md`), findingsMarkdown(summary, report));
|
|
129
|
-
return summary;
|
|
130
|
-
} catch (error) {
|
|
131
|
-
const details = errorDetails(error);
|
|
132
|
-
if (details.stdout) await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stdout.txt`), details.stdout);
|
|
133
|
-
if (details.stderr) await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stderr.txt`), details.stderr);
|
|
134
|
-
const summary = summarizeReport(input.id, input.command, Date.now() - startedAt, {}, [details.message]);
|
|
135
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}-findings.md`), findingsMarkdown(summary, {}));
|
|
136
|
-
return summary;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function summarizeReport(
|
|
141
|
-
id: string,
|
|
142
|
-
command: string,
|
|
143
|
-
elapsedMs: number,
|
|
144
|
-
report: Record<string, unknown>,
|
|
145
|
-
errors: readonly string[],
|
|
146
|
-
): ExperimentSummary {
|
|
147
|
-
const run = objectValue(report.run);
|
|
148
|
-
const auditStats = objectValue(run.auditStats);
|
|
149
|
-
const traceEvents = arrayValue(run.traceEvents).map(objectValue);
|
|
150
|
-
const findings = arrayValue(report.findings).map(objectValue);
|
|
151
|
-
const targetPreview = arrayValue(run.debugTargets).map(objectValue).map(debugTargetRecord);
|
|
152
|
-
const contextPackEvents = traceEvents.filter((event) => event.name === "context.pack");
|
|
153
|
-
const auditBatchEvents = traceEvents.filter((event) => event.name === "audit.batch");
|
|
154
|
-
const auditInputTokens = auditBatchEvents.map((event) => detailNumber(event.detail, "input_tokens")).filter(isNumber);
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
id,
|
|
158
|
-
command,
|
|
159
|
-
totalMs: numberValue(objectValue(run.timingsMs).total, elapsedMs),
|
|
160
|
-
entitiesScanned: numberValue(run.entitiesScanned, 0),
|
|
161
|
-
targets: numberValue(run.auditedCandidateCount, 0),
|
|
162
|
-
targetsByCheck: recordOfNumbers(run.targetsByCheck),
|
|
163
|
-
auditContext: stringValue(run.auditContext, "unknown"),
|
|
164
|
-
auditPrompt: stringValue(run.auditPrompt, "unknown"),
|
|
165
|
-
repomixFiles: maxNumber(contextPackEvents.map((event) => numberValue(event.count, 0))),
|
|
166
|
-
repomixTokens: maxNumber(contextPackEvents.map((event) => detailNumber(event.detail, "pack_tokens") ?? 0)),
|
|
167
|
-
auditCalls: auditBatchEvents.length,
|
|
168
|
-
auditInputTokens,
|
|
169
|
-
auditMs: numberValue(objectValue(run.timingsMs).audit, 0),
|
|
170
|
-
findings: findings.length,
|
|
171
|
-
uncertain: numberValue(auditStats.uncertain, 0),
|
|
172
|
-
clean: numberValue(auditStats.clean, 0),
|
|
173
|
-
findingsByCheck: countBy(findings, "checkId"),
|
|
174
|
-
uncertainByCheck: {},
|
|
175
|
-
targetPreview,
|
|
176
|
-
errors,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function findingsMarkdown(summary: ExperimentSummary, report: Record<string, unknown>): string {
|
|
181
|
-
const findings = arrayValue(report.findings).map(objectValue);
|
|
182
|
-
const lines = [
|
|
183
|
-
`# ${summary.id}`,
|
|
184
|
-
"",
|
|
185
|
-
`Runtime: ${Math.round(summary.totalMs / 1000)}s`,
|
|
186
|
-
`Targets: ${summary.targets}`,
|
|
187
|
-
`Findings: ${summary.findings}`,
|
|
188
|
-
`Uncertain: ${summary.uncertain}`,
|
|
189
|
-
"",
|
|
190
|
-
"## Targets",
|
|
191
|
-
...targetMarkdown(summary.targetPreview),
|
|
192
|
-
"",
|
|
193
|
-
"## Findings",
|
|
194
|
-
];
|
|
195
|
-
if (findings.length === 0) {
|
|
196
|
-
lines.push("None.");
|
|
197
|
-
} else {
|
|
198
|
-
findings.forEach((finding, index) => {
|
|
199
|
-
lines.push(
|
|
200
|
-
`${index + 1}. ${stringValue(finding.checkId, "unknown")}`,
|
|
201
|
-
`why: ${stringValue(finding.why, "")}`,
|
|
202
|
-
`proof: ${stringValue(finding.proof, "")}`,
|
|
203
|
-
"Manual label: [good/maybe/bad]",
|
|
204
|
-
"",
|
|
205
|
-
);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
if (summary.errors.length > 0) {
|
|
209
|
-
lines.push("", "## Errors", ...summary.errors.map((error) => `- ${error}`));
|
|
210
|
-
}
|
|
211
|
-
return `${lines.join("\n")}\n`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function targetMarkdown(targets: readonly DebugTargetRecord[]): readonly string[] {
|
|
215
|
-
if (targets.length === 0) return ["None recorded."];
|
|
216
|
-
return targets.map((target) => (
|
|
217
|
-
`- ${target.targetId} ${target.checkId} ${target.entityKind ?? "unknown"}/${target.changeKind ?? "unknown"} ${target.entityId}
|
|
218
|
-
reason: ${target.scoutReason ?? ""}`
|
|
219
|
-
));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function countBy(items: readonly Record<string, unknown>[], key: string): Record<string, number> {
|
|
223
|
-
const counts: Record<string, number> = {};
|
|
224
|
-
for (const item of items) {
|
|
225
|
-
const value = stringValue(item[key], "");
|
|
226
|
-
if (!value) continue;
|
|
227
|
-
counts[value] = (counts[value] ?? 0) + 1;
|
|
228
|
-
}
|
|
229
|
-
return counts;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function recordOfNumbers(value: unknown): Record<string, number> {
|
|
233
|
-
const input = objectValue(value);
|
|
234
|
-
const output: Record<string, number> = {};
|
|
235
|
-
for (const [key, count] of Object.entries(input)) {
|
|
236
|
-
if (typeof count === "number" && Number.isFinite(count)) output[key] = count;
|
|
237
|
-
}
|
|
238
|
-
return output;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function debugTargetRecord(value: Record<string, unknown>): DebugTargetRecord {
|
|
242
|
-
return {
|
|
243
|
-
targetId: stringValue(value.targetId, ""),
|
|
244
|
-
checkId: stringValue(value.checkId, ""),
|
|
245
|
-
entityId: stringValue(value.entityId, ""),
|
|
246
|
-
entityKind: optionalString(value.entityKind),
|
|
247
|
-
changeKind: optionalString(value.changeKind),
|
|
248
|
-
scoutReason: optionalString(value.scoutReason),
|
|
249
|
-
sourceLabel: optionalString(value.sourceLabel),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function optionalString(value: unknown): string | undefined {
|
|
254
|
-
return typeof value === "string" ? value : undefined;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function ensureJson(args: string[]): string[] {
|
|
258
|
-
return args.includes("--json") ? args : [...args, "--json"];
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function resolveCwd(value: string | undefined): string {
|
|
262
|
-
if (!value) return process.cwd();
|
|
263
|
-
if (value.startsWith("$")) {
|
|
264
|
-
const envName = value.slice(1);
|
|
265
|
-
const envValue = process.env[envName];
|
|
266
|
-
if (!envValue) throw new Error(`Experiment cwd env var ${envName} is not set.`);
|
|
267
|
-
return path.resolve(envValue);
|
|
268
|
-
}
|
|
269
|
-
return path.resolve(value);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function objectValue(value: unknown): Record<string, unknown> {
|
|
273
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function arrayValue(value: unknown): readonly unknown[] {
|
|
277
|
-
return Array.isArray(value) ? value : [];
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function stringValue(value: unknown, fallback: string): string {
|
|
281
|
-
return typeof value === "string" ? value : fallback;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function numberValue(value: unknown, fallback: number): number {
|
|
285
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function detailNumber(value: unknown, key: string): number | null {
|
|
289
|
-
if (typeof value !== "string") return null;
|
|
290
|
-
const match = new RegExp(`${key}=([0-9]+)`).exec(value);
|
|
291
|
-
return match ? Number(match[1]) : null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function maxNumber(values: readonly number[]): number {
|
|
295
|
-
return values.length === 0 ? 0 : Math.max(...values);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function isNumber(value: number | null): value is number {
|
|
299
|
-
return value !== null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function errorDetails(error: unknown): Readonly<{ message: string; stdout?: string; stderr?: string }> {
|
|
303
|
-
const candidate = objectValue(error);
|
|
304
|
-
return {
|
|
305
|
-
message: error instanceof Error ? error.message : String(error),
|
|
306
|
-
stdout: typeof candidate.stdout === "string" ? candidate.stdout : undefined,
|
|
307
|
-
stderr: typeof candidate.stderr === "string" ? candidate.stderr : undefined,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function timestamp(date: Date): string {
|
|
312
|
-
return date.toISOString().replace(/[:.]/g, "-");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function safeSegment(value: string): string {
|
|
316
|
-
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "experiment";
|
|
317
|
-
}
|