@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/dist/experiment.js
DELETED
|
@@ -1,225 +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
|
-
const execFileAsync = promisify(execFile);
|
|
6
|
-
export async function runExperiment(configPath) {
|
|
7
|
-
const config = await readConfig(configPath);
|
|
8
|
-
const startedAt = new Date();
|
|
9
|
-
const outputDir = path.join(process.cwd(), "experiments", "results", `${safeSegment(config.name)}-${timestamp(startedAt)}`);
|
|
10
|
-
await mkdir(outputDir, { recursive: true });
|
|
11
|
-
const cwd = resolveCwd(config.cwd);
|
|
12
|
-
const cliPath = path.resolve(process.argv[1] ?? "packages/cli/dist/stupify.js");
|
|
13
|
-
const summaries = [];
|
|
14
|
-
for (const run of config.runs) {
|
|
15
|
-
const args = ensureJson([...config.baseCommand, ...run.args]);
|
|
16
|
-
const command = [process.execPath, cliPath, ...args].join(" ");
|
|
17
|
-
const summary = await runOneExperiment({
|
|
18
|
-
id: run.id,
|
|
19
|
-
args,
|
|
20
|
-
command,
|
|
21
|
-
cwd,
|
|
22
|
-
cliPath,
|
|
23
|
-
outputDir,
|
|
24
|
-
});
|
|
25
|
-
summaries.push(summary);
|
|
26
|
-
await writeFile(path.join(outputDir, "summary.json"), `${JSON.stringify({ name: config.name, cwd, runs: summaries }, null, 2)}\n`);
|
|
27
|
-
}
|
|
28
|
-
return outputDir;
|
|
29
|
-
}
|
|
30
|
-
async function readConfig(configPath) {
|
|
31
|
-
const fullPath = path.resolve(configPath);
|
|
32
|
-
const raw = await readFile(fullPath, "utf8");
|
|
33
|
-
const parsed = JSON.parse(raw);
|
|
34
|
-
if (!parsed.name)
|
|
35
|
-
throw new Error("Experiment config requires name.");
|
|
36
|
-
if (!Array.isArray(parsed.baseCommand))
|
|
37
|
-
throw new Error("Experiment config requires baseCommand array.");
|
|
38
|
-
if (!Array.isArray(parsed.runs))
|
|
39
|
-
throw new Error("Experiment config requires runs array.");
|
|
40
|
-
return {
|
|
41
|
-
name: parsed.name,
|
|
42
|
-
cwd: parsed.cwd,
|
|
43
|
-
baseCommand: parsed.baseCommand,
|
|
44
|
-
runs: parsed.runs,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
async function runOneExperiment(input) {
|
|
48
|
-
const startedAt = Date.now();
|
|
49
|
-
try {
|
|
50
|
-
const { stdout, stderr } = await execFileAsync(process.execPath, [input.cliPath, ...input.args], {
|
|
51
|
-
cwd: input.cwd,
|
|
52
|
-
maxBuffer: 128 * 1024 * 1024,
|
|
53
|
-
});
|
|
54
|
-
if (stderr.trim()) {
|
|
55
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stderr.txt`), stderr);
|
|
56
|
-
}
|
|
57
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.json`), stdout);
|
|
58
|
-
const report = JSON.parse(stdout);
|
|
59
|
-
const summary = summarizeReport(input.id, input.command, Date.now() - startedAt, report, []);
|
|
60
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}-findings.md`), findingsMarkdown(summary, report));
|
|
61
|
-
return summary;
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
const details = errorDetails(error);
|
|
65
|
-
if (details.stdout)
|
|
66
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stdout.txt`), details.stdout);
|
|
67
|
-
if (details.stderr)
|
|
68
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}.stderr.txt`), details.stderr);
|
|
69
|
-
const summary = summarizeReport(input.id, input.command, Date.now() - startedAt, {}, [details.message]);
|
|
70
|
-
await writeFile(path.join(input.outputDir, `${safeSegment(input.id)}-findings.md`), findingsMarkdown(summary, {}));
|
|
71
|
-
return summary;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function summarizeReport(id, command, elapsedMs, report, errors) {
|
|
75
|
-
const run = objectValue(report.run);
|
|
76
|
-
const auditStats = objectValue(run.auditStats);
|
|
77
|
-
const traceEvents = arrayValue(run.traceEvents).map(objectValue);
|
|
78
|
-
const findings = arrayValue(report.findings).map(objectValue);
|
|
79
|
-
const targetPreview = arrayValue(run.debugTargets).map(objectValue).map(debugTargetRecord);
|
|
80
|
-
const contextPackEvents = traceEvents.filter((event) => event.name === "context.pack");
|
|
81
|
-
const auditBatchEvents = traceEvents.filter((event) => event.name === "audit.batch");
|
|
82
|
-
const auditInputTokens = auditBatchEvents.map((event) => detailNumber(event.detail, "input_tokens")).filter(isNumber);
|
|
83
|
-
return {
|
|
84
|
-
id,
|
|
85
|
-
command,
|
|
86
|
-
totalMs: numberValue(objectValue(run.timingsMs).total, elapsedMs),
|
|
87
|
-
entitiesScanned: numberValue(run.entitiesScanned, 0),
|
|
88
|
-
targets: numberValue(run.auditedCandidateCount, 0),
|
|
89
|
-
targetsByCheck: recordOfNumbers(run.targetsByCheck),
|
|
90
|
-
auditContext: stringValue(run.auditContext, "unknown"),
|
|
91
|
-
auditPrompt: stringValue(run.auditPrompt, "unknown"),
|
|
92
|
-
repomixFiles: maxNumber(contextPackEvents.map((event) => numberValue(event.count, 0))),
|
|
93
|
-
repomixTokens: maxNumber(contextPackEvents.map((event) => detailNumber(event.detail, "pack_tokens") ?? 0)),
|
|
94
|
-
auditCalls: auditBatchEvents.length,
|
|
95
|
-
auditInputTokens,
|
|
96
|
-
auditMs: numberValue(objectValue(run.timingsMs).audit, 0),
|
|
97
|
-
findings: findings.length,
|
|
98
|
-
uncertain: numberValue(auditStats.uncertain, 0),
|
|
99
|
-
clean: numberValue(auditStats.clean, 0),
|
|
100
|
-
findingsByCheck: countBy(findings, "checkId"),
|
|
101
|
-
uncertainByCheck: {},
|
|
102
|
-
targetPreview,
|
|
103
|
-
errors,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function findingsMarkdown(summary, report) {
|
|
107
|
-
const findings = arrayValue(report.findings).map(objectValue);
|
|
108
|
-
const lines = [
|
|
109
|
-
`# ${summary.id}`,
|
|
110
|
-
"",
|
|
111
|
-
`Runtime: ${Math.round(summary.totalMs / 1000)}s`,
|
|
112
|
-
`Targets: ${summary.targets}`,
|
|
113
|
-
`Findings: ${summary.findings}`,
|
|
114
|
-
`Uncertain: ${summary.uncertain}`,
|
|
115
|
-
"",
|
|
116
|
-
"## Targets",
|
|
117
|
-
...targetMarkdown(summary.targetPreview),
|
|
118
|
-
"",
|
|
119
|
-
"## Findings",
|
|
120
|
-
];
|
|
121
|
-
if (findings.length === 0) {
|
|
122
|
-
lines.push("None.");
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
findings.forEach((finding, index) => {
|
|
126
|
-
lines.push(`${index + 1}. ${stringValue(finding.checkId, "unknown")}`, `why: ${stringValue(finding.why, "")}`, `proof: ${stringValue(finding.proof, "")}`, "Manual label: [good/maybe/bad]", "");
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
if (summary.errors.length > 0) {
|
|
130
|
-
lines.push("", "## Errors", ...summary.errors.map((error) => `- ${error}`));
|
|
131
|
-
}
|
|
132
|
-
return `${lines.join("\n")}\n`;
|
|
133
|
-
}
|
|
134
|
-
function targetMarkdown(targets) {
|
|
135
|
-
if (targets.length === 0)
|
|
136
|
-
return ["None recorded."];
|
|
137
|
-
return targets.map((target) => (`- ${target.targetId} ${target.checkId} ${target.entityKind ?? "unknown"}/${target.changeKind ?? "unknown"} ${target.entityId}
|
|
138
|
-
reason: ${target.scoutReason ?? ""}`));
|
|
139
|
-
}
|
|
140
|
-
function countBy(items, key) {
|
|
141
|
-
const counts = {};
|
|
142
|
-
for (const item of items) {
|
|
143
|
-
const value = stringValue(item[key], "");
|
|
144
|
-
if (!value)
|
|
145
|
-
continue;
|
|
146
|
-
counts[value] = (counts[value] ?? 0) + 1;
|
|
147
|
-
}
|
|
148
|
-
return counts;
|
|
149
|
-
}
|
|
150
|
-
function recordOfNumbers(value) {
|
|
151
|
-
const input = objectValue(value);
|
|
152
|
-
const output = {};
|
|
153
|
-
for (const [key, count] of Object.entries(input)) {
|
|
154
|
-
if (typeof count === "number" && Number.isFinite(count))
|
|
155
|
-
output[key] = count;
|
|
156
|
-
}
|
|
157
|
-
return output;
|
|
158
|
-
}
|
|
159
|
-
function debugTargetRecord(value) {
|
|
160
|
-
return {
|
|
161
|
-
targetId: stringValue(value.targetId, ""),
|
|
162
|
-
checkId: stringValue(value.checkId, ""),
|
|
163
|
-
entityId: stringValue(value.entityId, ""),
|
|
164
|
-
entityKind: optionalString(value.entityKind),
|
|
165
|
-
changeKind: optionalString(value.changeKind),
|
|
166
|
-
scoutReason: optionalString(value.scoutReason),
|
|
167
|
-
sourceLabel: optionalString(value.sourceLabel),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
function optionalString(value) {
|
|
171
|
-
return typeof value === "string" ? value : undefined;
|
|
172
|
-
}
|
|
173
|
-
function ensureJson(args) {
|
|
174
|
-
return args.includes("--json") ? args : [...args, "--json"];
|
|
175
|
-
}
|
|
176
|
-
function resolveCwd(value) {
|
|
177
|
-
if (!value)
|
|
178
|
-
return process.cwd();
|
|
179
|
-
if (value.startsWith("$")) {
|
|
180
|
-
const envName = value.slice(1);
|
|
181
|
-
const envValue = process.env[envName];
|
|
182
|
-
if (!envValue)
|
|
183
|
-
throw new Error(`Experiment cwd env var ${envName} is not set.`);
|
|
184
|
-
return path.resolve(envValue);
|
|
185
|
-
}
|
|
186
|
-
return path.resolve(value);
|
|
187
|
-
}
|
|
188
|
-
function objectValue(value) {
|
|
189
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
190
|
-
}
|
|
191
|
-
function arrayValue(value) {
|
|
192
|
-
return Array.isArray(value) ? value : [];
|
|
193
|
-
}
|
|
194
|
-
function stringValue(value, fallback) {
|
|
195
|
-
return typeof value === "string" ? value : fallback;
|
|
196
|
-
}
|
|
197
|
-
function numberValue(value, fallback) {
|
|
198
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
199
|
-
}
|
|
200
|
-
function detailNumber(value, key) {
|
|
201
|
-
if (typeof value !== "string")
|
|
202
|
-
return null;
|
|
203
|
-
const match = new RegExp(`${key}=([0-9]+)`).exec(value);
|
|
204
|
-
return match ? Number(match[1]) : null;
|
|
205
|
-
}
|
|
206
|
-
function maxNumber(values) {
|
|
207
|
-
return values.length === 0 ? 0 : Math.max(...values);
|
|
208
|
-
}
|
|
209
|
-
function isNumber(value) {
|
|
210
|
-
return value !== null;
|
|
211
|
-
}
|
|
212
|
-
function errorDetails(error) {
|
|
213
|
-
const candidate = objectValue(error);
|
|
214
|
-
return {
|
|
215
|
-
message: error instanceof Error ? error.message : String(error),
|
|
216
|
-
stdout: typeof candidate.stdout === "string" ? candidate.stdout : undefined,
|
|
217
|
-
stderr: typeof candidate.stderr === "string" ? candidate.stderr : undefined,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
function timestamp(date) {
|
|
221
|
-
return date.toISOString().replace(/[:.]/g, "-");
|
|
222
|
-
}
|
|
223
|
-
function safeSegment(value) {
|
|
224
|
-
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "experiment";
|
|
225
|
-
}
|
package/src/batcher.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import type { DiffBatch, DiffHunk } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_BATCH_LINES = 1_000;
|
|
4
|
-
|
|
5
|
-
type ParsedHunk = Readonly<{
|
|
6
|
-
fileId: string;
|
|
7
|
-
hunkId: string;
|
|
8
|
-
filePath: string;
|
|
9
|
-
text: string;
|
|
10
|
-
lineCount: number;
|
|
11
|
-
}>;
|
|
12
|
-
|
|
13
|
-
export function batchDiff(
|
|
14
|
-
diff: string,
|
|
15
|
-
linesPerBatch = DEFAULT_BATCH_LINES,
|
|
16
|
-
): readonly DiffBatch[] {
|
|
17
|
-
const hunks = parseHunks(diff).flatMap((item) =>
|
|
18
|
-
splitLargeHunk(item, linesPerBatch),
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
type BatchState = Readonly<{
|
|
22
|
-
batches: readonly DiffBatch[];
|
|
23
|
-
current: readonly DiffHunk[];
|
|
24
|
-
currentLines: number;
|
|
25
|
-
batchNumber: number;
|
|
26
|
-
}>;
|
|
27
|
-
|
|
28
|
-
const initialState: BatchState = {
|
|
29
|
-
batches: [],
|
|
30
|
-
current: [],
|
|
31
|
-
currentLines: 0,
|
|
32
|
-
batchNumber: 1,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const finalized = hunks.reduce<BatchState>((state, hunk) => {
|
|
36
|
-
const needsFlush =
|
|
37
|
-
state.current.length > 0 &&
|
|
38
|
-
state.currentLines + hunk.lineCount > linesPerBatch;
|
|
39
|
-
|
|
40
|
-
const flushed = needsFlush ? flush(state) : state;
|
|
41
|
-
return {
|
|
42
|
-
...flushed,
|
|
43
|
-
current: [...flushed.current, toDiffHunk(hunk, flushed.batchNumber)],
|
|
44
|
-
currentLines: flushed.currentLines + hunk.lineCount,
|
|
45
|
-
};
|
|
46
|
-
}, initialState);
|
|
47
|
-
|
|
48
|
-
return finalized.current.length > 0
|
|
49
|
-
? [...finalized.batches, toBatch(finalized.batchNumber, finalized.current)]
|
|
50
|
-
: finalized.batches;
|
|
51
|
-
|
|
52
|
-
function flush(state: BatchState): BatchState {
|
|
53
|
-
return {
|
|
54
|
-
batches: [...state.batches, toBatch(state.batchNumber, state.current)],
|
|
55
|
-
current: [],
|
|
56
|
-
currentLines: 0,
|
|
57
|
-
batchNumber: state.batchNumber + 1,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function allHunks(batches: readonly DiffBatch[]): readonly DiffHunk[] {
|
|
63
|
-
return batches.flatMap((batch) => batch.hunks);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parseHunks(diff: string): readonly ParsedHunk[] {
|
|
67
|
-
type ParseState = Readonly<{
|
|
68
|
-
hunks: readonly ParsedHunk[];
|
|
69
|
-
filePath: string;
|
|
70
|
-
fileIndex: number;
|
|
71
|
-
hunkIndex: number;
|
|
72
|
-
fileHeader: readonly string[];
|
|
73
|
-
hunkLines: readonly string[] | null;
|
|
74
|
-
}>;
|
|
75
|
-
|
|
76
|
-
const lines = diff.split(/\r?\n/);
|
|
77
|
-
|
|
78
|
-
const initialState: ParseState = {
|
|
79
|
-
hunks: [],
|
|
80
|
-
filePath: "unknown",
|
|
81
|
-
fileIndex: 0,
|
|
82
|
-
hunkIndex: 0,
|
|
83
|
-
fileHeader: [],
|
|
84
|
-
hunkLines: null,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const finalState = lines.reduce<ParseState>((state, line) => {
|
|
88
|
-
const fileMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
|
|
89
|
-
if (fileMatch) {
|
|
90
|
-
const flushed = flush(state);
|
|
91
|
-
return {
|
|
92
|
-
...flushed,
|
|
93
|
-
fileIndex: flushed.fileIndex + 1,
|
|
94
|
-
hunkIndex: 0,
|
|
95
|
-
filePath: fileMatch[1],
|
|
96
|
-
fileHeader: [line],
|
|
97
|
-
hunkLines: null,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (line.startsWith("@@ ")) {
|
|
102
|
-
const flushed = flush(state);
|
|
103
|
-
return {
|
|
104
|
-
...flushed,
|
|
105
|
-
hunkIndex: flushed.hunkIndex + 1,
|
|
106
|
-
hunkLines: [...flushed.fileHeader, line],
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (state.hunkLines) return { ...state, hunkLines: [...state.hunkLines, line] };
|
|
111
|
-
if (state.fileHeader.length > 0) return { ...state, fileHeader: [...state.fileHeader, line] };
|
|
112
|
-
return state;
|
|
113
|
-
}, initialState);
|
|
114
|
-
|
|
115
|
-
return flush(finalState).hunks;
|
|
116
|
-
|
|
117
|
-
function flush(state: ParseState): ParseState {
|
|
118
|
-
if (!state.hunkLines) return state;
|
|
119
|
-
const fileId = `file-${pad(state.fileIndex)}`;
|
|
120
|
-
const hunkId = `hunk-${pad(state.hunkIndex)}`;
|
|
121
|
-
const text = state.hunkLines.join("\n").trimEnd();
|
|
122
|
-
const nextHunk: ParsedHunk = {
|
|
123
|
-
fileId,
|
|
124
|
-
hunkId,
|
|
125
|
-
filePath: state.filePath,
|
|
126
|
-
text,
|
|
127
|
-
lineCount: countLines(text),
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return { ...state, hunks: [...state.hunks, nextHunk], hunkLines: null };
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function splitLargeHunk(
|
|
135
|
-
hunk: ParsedHunk,
|
|
136
|
-
linesPerBatch: number,
|
|
137
|
-
): readonly ParsedHunk[] {
|
|
138
|
-
if (hunk.lineCount <= linesPerBatch) return [hunk];
|
|
139
|
-
const lines = hunk.text.split(/\r?\n/);
|
|
140
|
-
const chunkCount = Math.ceil(lines.length / linesPerBatch);
|
|
141
|
-
return Array.from({ length: chunkCount }, (_, chunkIndex) => {
|
|
142
|
-
const start = chunkIndex * linesPerBatch;
|
|
143
|
-
const text = lines.slice(start, start + linesPerBatch).join("\n");
|
|
144
|
-
return {
|
|
145
|
-
...hunk,
|
|
146
|
-
hunkId: `${hunk.hunkId}-part-${pad(chunkIndex + 1)}`,
|
|
147
|
-
text,
|
|
148
|
-
lineCount: countLines(text),
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function toDiffHunk(hunk: ParsedHunk, batchNumber: number): DiffHunk {
|
|
154
|
-
const batchId = `batch-${pad(batchNumber)}`;
|
|
155
|
-
return {
|
|
156
|
-
...hunk,
|
|
157
|
-
batchId,
|
|
158
|
-
pointer: `${batchId}:${hunk.fileId}:${hunk.hunkId}`,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function toBatch(batchNumber: number, hunks: readonly DiffHunk[]): DiffBatch {
|
|
163
|
-
const id = `batch-${pad(batchNumber)}`;
|
|
164
|
-
return {
|
|
165
|
-
id,
|
|
166
|
-
hunks,
|
|
167
|
-
text: hunks.map(formatHunkForSearch).join("\n\n"),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function formatHunkForSearch(hunk: DiffHunk): string {
|
|
172
|
-
return `POINTER ${hunk.pointer}
|
|
173
|
-
FILE ${hunk.fileId}
|
|
174
|
-
PATH ${hunk.filePath}
|
|
175
|
-
${searchView(hunk.text)}`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function searchView(text: string): string {
|
|
179
|
-
return text
|
|
180
|
-
.split(/\r?\n/)
|
|
181
|
-
.filter(
|
|
182
|
-
(line) =>
|
|
183
|
-
line.startsWith("diff --git ") ||
|
|
184
|
-
line.startsWith("--- ") ||
|
|
185
|
-
line.startsWith("+++ ") ||
|
|
186
|
-
line.startsWith("@@ ") ||
|
|
187
|
-
(line.startsWith("+") && !line.startsWith("+++")),
|
|
188
|
-
)
|
|
189
|
-
.join("\n");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function countLines(value: string): number {
|
|
193
|
-
return value.length === 0 ? 0 : value.split(/\r?\n/).length;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function pad(value: number): string {
|
|
197
|
-
return String(value).padStart(3, "0");
|
|
198
|
-
}
|
package/src/candidate-context.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { allHunks } from "./batcher.ts";
|
|
2
|
-
import type { CandidateContext, DiffBatch, DiffHunk } from "./types.ts";
|
|
3
|
-
|
|
4
|
-
const MAX_CONTEXT_LINES = 80;
|
|
5
|
-
|
|
6
|
-
export function candidateContexts(
|
|
7
|
-
batches: readonly DiffBatch[],
|
|
8
|
-
candidatePointers: readonly string[],
|
|
9
|
-
): readonly CandidateContext[] {
|
|
10
|
-
const hunks = allHunks(batches);
|
|
11
|
-
const byPointer = new Map(hunks.map((hunk) => [hunk.pointer, hunk]));
|
|
12
|
-
const uniquePointers = [...new Set(candidatePointers)]
|
|
13
|
-
.sort((left, right) => hunkPriority(byPointer.get(right)) - hunkPriority(byPointer.get(left)));
|
|
14
|
-
|
|
15
|
-
return uniquePointers.flatMap((pointer) => {
|
|
16
|
-
const hunk = byPointer.get(pointer);
|
|
17
|
-
if (!hunk) return [];
|
|
18
|
-
return [{ pointer, text: formatHunk(hunk) }];
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function formatHunk(hunk: DiffHunk): string {
|
|
23
|
-
return `PATH ${hunk.filePath}
|
|
24
|
-
${shorten(hunk.text)}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function shorten(text: string): string {
|
|
28
|
-
const lines = text.split(/\r?\n/);
|
|
29
|
-
if (lines.length <= MAX_CONTEXT_LINES) return text;
|
|
30
|
-
return `${lines.slice(0, MAX_CONTEXT_LINES).join("\n")}
|
|
31
|
-
[stupify: hunk shortened after ${MAX_CONTEXT_LINES} lines]`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function hunkPriority(hunk: DiffHunk | undefined): number {
|
|
35
|
-
if (!hunk) return 0;
|
|
36
|
-
const text = hunk.text;
|
|
37
|
-
let priority = 0;
|
|
38
|
-
if (/^\+export\s+type\s|\+export\s+interface\s|\+type\s|\+interface\s/m.test(text)) priority += 3;
|
|
39
|
-
if (/^\+export\s+function\s|\+function\s/m.test(text)) priority += 2;
|
|
40
|
-
if (/\.map\(|=>\s*\(\{|=>\s*\{/m.test(text)) priority += 2;
|
|
41
|
-
if (/payload|schema|dto|response|result/i.test(text)) priority += 1;
|
|
42
|
-
return priority;
|
|
43
|
-
}
|