@stupify/cli 0.0.3 → 0.0.5
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 +185 -334
- 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 +215 -527
- 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/stupify.js
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { candidateContexts } from "./candidate-context.js";
|
|
6
|
-
import { enabledChecks } from "./checks.js";
|
|
4
|
+
import { countPromptTokens, runSearch, searchRequest } from "./analysis.js";
|
|
5
|
+
import { searchChecks } from "./checks.js";
|
|
7
6
|
import { parseCommand } from "./command.js";
|
|
8
|
-
import { MODEL_REGISTRY } from "./constants.js";
|
|
9
7
|
import { counterScoutTargets } from "./counter-scout.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
8
|
+
import { runDoctor } from "./doctor.js";
|
|
9
|
+
import { runHookCommand } from "./hooks.js";
|
|
10
|
+
import { firstRunModelBootstrap, loadLocalModel } from "./model.js";
|
|
11
|
+
import { entityContextsFromChanges, emptyContextPack, repomixContextPack, repomixSearchConfig } from "./repomix-provider.js";
|
|
12
|
+
import { helpText, renderSearchRun } from "./render.js";
|
|
13
|
+
import { effectiveMaxCandidates, effectiveMaxSearchInputTokens, effectiveRepomixConfig, effectiveSearchChecks, loadSearchProfile, } from "./search-profile.js";
|
|
16
14
|
import { semChangeSetForCommand } from "./sem-provider.js";
|
|
17
|
-
import { createTracer
|
|
18
|
-
const SEM_SCOUT_CHUNK_SIZE = 200;
|
|
15
|
+
import { createTracer } from "./trace.js";
|
|
19
16
|
export async function main(argv = process.argv.slice(2)) {
|
|
20
17
|
const startedAt = Date.now();
|
|
21
18
|
try {
|
|
@@ -24,16 +21,22 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
24
21
|
console.log(helpText());
|
|
25
22
|
return 0;
|
|
26
23
|
}
|
|
27
|
-
if (command.kind === "
|
|
28
|
-
|
|
29
|
-
console.log(`Experiment results written to ${outputDir}`);
|
|
24
|
+
if (command.kind === "hook") {
|
|
25
|
+
console.log(await runHookCommand(command.action));
|
|
30
26
|
return 0;
|
|
31
27
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
if (command.kind === "doctor") {
|
|
29
|
+
const result = await runDoctor();
|
|
30
|
+
console.log(result.text);
|
|
31
|
+
return result.exitCode;
|
|
32
|
+
}
|
|
33
|
+
if (command.kind === "bench-search") {
|
|
34
|
+
const { runSearchBench } = await import("./search-bench.js");
|
|
35
|
+
console.log(await runSearchBench(command.configPath));
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
const run = await runSearchCommand(command, startedAt);
|
|
39
|
+
console.log(renderSearchRun(run, command));
|
|
37
40
|
return 0;
|
|
38
41
|
}
|
|
39
42
|
catch (error) {
|
|
@@ -41,347 +44,195 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
41
44
|
return 1;
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
|
-
async function
|
|
45
|
-
const { value: diff, ms: diffMs } = await trace.trace("net.diff", () => netDiffForCommand(command));
|
|
46
|
-
printRunPlan(command, diff, checks.map((check) => check.id));
|
|
47
|
-
const { value: models, ms: modelMs } = await trace.trace("model.load", () => loadLocalModels(command.model));
|
|
48
|
-
const { scoutModel, auditModel } = models;
|
|
49
|
-
const batches = batchDiff(diff.text);
|
|
50
|
-
const { value: candidatePointers, ms: searchMs } = await trace.trace("search.total", async () => {
|
|
51
|
-
const pointers = [];
|
|
52
|
-
for (const batch of batches) {
|
|
53
|
-
const { value: candidates } = await trace.trace("search.batch", () => scoutBatch(scoutModel, batch, checks, diff.label), { fields: { batch: batch.id } });
|
|
54
|
-
pointers.push(...candidates);
|
|
55
|
-
}
|
|
56
|
-
return pointers;
|
|
57
|
-
});
|
|
58
|
-
const contexts = candidateContexts(batches, candidatePointers);
|
|
59
|
-
const auditedContexts = contexts;
|
|
60
|
-
const { value: result, ms: auditMs } = await trace.trace("audit.candidates", () => auditCandidates(auditModel, diff, auditedContexts, checks), { fields: { candidates: auditedContexts.length } });
|
|
61
|
-
return {
|
|
62
|
-
run: {
|
|
63
|
-
mode: command.kind,
|
|
64
|
-
engine: command.engine,
|
|
65
|
-
auditContext: command.auditContext,
|
|
66
|
-
auditPrompt: command.auditPrompt,
|
|
67
|
-
modelId: command.model,
|
|
68
|
-
checkIds: checks.map((check) => check.id),
|
|
69
|
-
sourceId: diff.id,
|
|
70
|
-
label: diff.label,
|
|
71
|
-
stats: diff.stats,
|
|
72
|
-
batchesScanned: batches.length,
|
|
73
|
-
candidateCount: new Set(candidatePointers).size,
|
|
74
|
-
entitiesScanned: 0,
|
|
75
|
-
auditedCandidateCount: auditedContexts.length,
|
|
76
|
-
scoutModelCalls: batches.length,
|
|
77
|
-
auditModelCalls: auditedContexts.length > 0 ? 1 : 0,
|
|
78
|
-
warnings: [],
|
|
79
|
-
timingsMs: {
|
|
80
|
-
diff: diffMs,
|
|
81
|
-
modelLoad: modelMs,
|
|
82
|
-
search: searchMs,
|
|
83
|
-
audit: auditMs,
|
|
84
|
-
total: Date.now() - startedAt,
|
|
85
|
-
},
|
|
86
|
-
debugTargets: command.debugTargets ? [] : undefined,
|
|
87
|
-
},
|
|
88
|
-
result,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
async function runSemEngine(command, checks, startedAt) {
|
|
92
|
-
const traceEvents = [];
|
|
47
|
+
export async function runSearchCommand(command, startedAt) {
|
|
93
48
|
const t = createTracer({
|
|
49
|
+
writeLine: () => undefined,
|
|
94
50
|
onEvent: (event) => {
|
|
95
|
-
|
|
96
|
-
|
|
51
|
+
const parts = [`trace ${event.name}`, `${event.ms}ms`];
|
|
52
|
+
if (event.count !== undefined)
|
|
53
|
+
parts.push(`count=${event.count}`);
|
|
54
|
+
if (event.detail)
|
|
55
|
+
parts.push(event.detail);
|
|
56
|
+
console.error(parts.join(" "));
|
|
97
57
|
},
|
|
98
58
|
});
|
|
99
|
-
const
|
|
59
|
+
const profile = await loadSearchProfile(command.searchProfilePath);
|
|
60
|
+
const checks = profile ? effectiveSearchChecks(command.checkIds, profile) : searchChecks(command.checkIds);
|
|
61
|
+
const patternIds = checks.map((check) => check.id);
|
|
62
|
+
const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
|
|
63
|
+
const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
|
|
64
|
+
const { value: changeSet } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
|
|
100
65
|
count: (v) => v.summary.total,
|
|
101
66
|
detail: (v) => `${v.summary.fileCount} files`,
|
|
102
67
|
});
|
|
103
|
-
printSemRunPlan(command, changeSet, checks.map((check) => check.id));
|
|
104
|
-
const { value: models, ms: modelMs } = await t.trace("model.load", () => loadLocalModels(command.model), {
|
|
105
|
-
count: () => 2,
|
|
106
|
-
detail: () => "scout+audit",
|
|
107
|
-
});
|
|
108
|
-
const { scoutModel, auditModel } = models;
|
|
109
68
|
try {
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
detail: (v) => `${new Set(v.map((context) => context.filePath).filter(Boolean)).size} files`,
|
|
123
|
-
});
|
|
124
|
-
const auditBatches = chunkSemContexts(contexts, command.auditBatchSize);
|
|
125
|
-
const { value: result, ms: auditMs } = await t.trace("audit.total", () => findingsAuditBatches(auditModel, changeSet, auditBatches, checks, traceEvents, t, command), {
|
|
126
|
-
count: (v) => v.findings.length,
|
|
127
|
-
detail: (v) => `${auditBatches.length} batches targets=${v.stats.totalTargets} clean=${v.stats.clean} uncertain=${v.stats.uncertain} invalid=${v.stats.invalid}`,
|
|
128
|
-
});
|
|
129
|
-
return {
|
|
130
|
-
run: {
|
|
131
|
-
mode: command.kind,
|
|
132
|
-
engine: command.engine,
|
|
133
|
-
auditContext: command.auditContext,
|
|
134
|
-
auditPrompt: command.auditPrompt,
|
|
135
|
-
modelId: command.model,
|
|
136
|
-
checkIds: checks.map((check) => check.id),
|
|
137
|
-
sourceId: changeSet.id,
|
|
138
|
-
label: changeSet.label,
|
|
69
|
+
printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
|
|
70
|
+
const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
|
|
71
|
+
const contexts = entityContextsFromChanges(candidates, changeSet.changes);
|
|
72
|
+
const targetsByPattern = countTargetsByPattern(contexts);
|
|
73
|
+
const targetsPreview = previewTargets(contexts);
|
|
74
|
+
if (contexts.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
schemaVersion: "search.v1",
|
|
77
|
+
mode: "search",
|
|
78
|
+
source: command.source,
|
|
79
|
+
model: { id: command.model },
|
|
80
|
+
patterns: patternIds,
|
|
139
81
|
stats: {
|
|
82
|
+
elapsedMs: Date.now() - startedAt,
|
|
83
|
+
modelCalls: 0,
|
|
84
|
+
skipped: true,
|
|
85
|
+
skipReason: "no_candidates",
|
|
140
86
|
filesChanged: changeSet.summary.fileCount,
|
|
141
|
-
|
|
142
|
-
|
|
87
|
+
entitiesScanned: changeSet.summary.total,
|
|
88
|
+
candidates: 0,
|
|
89
|
+
searchTargets: 0,
|
|
90
|
+
repomixFiles: 0,
|
|
91
|
+
repomixTokens: 0,
|
|
92
|
+
profileId: profile?.id,
|
|
93
|
+
targetsByPattern,
|
|
94
|
+
targetsPreview,
|
|
143
95
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
96
|
+
matches: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const baseRepomixConfig = effectiveRepomixConfig(repomixSearchConfig(), profile);
|
|
100
|
+
const initialPack = profile?.context === "sem"
|
|
101
|
+
? emptyContextPack()
|
|
102
|
+
: await t.trace("context.pack", () => repomixContextPack(changeSet.contextCwd, contexts, changeSet.changes, baseRepomixConfig), {
|
|
103
|
+
count: (v) => v.filePaths.length,
|
|
104
|
+
detail: (v) => `${v.totalTokens} tokens`,
|
|
105
|
+
}).then((result) => result.value);
|
|
106
|
+
const packedFiles = new Set(initialPack.filePaths);
|
|
107
|
+
const searchContexts = profile?.context === "sem"
|
|
108
|
+
? contexts
|
|
109
|
+
: contexts.filter((context) => context.filePath && packedFiles.has(context.filePath));
|
|
110
|
+
if (searchContexts.length === 0) {
|
|
111
|
+
return {
|
|
112
|
+
schemaVersion: "search.v1",
|
|
113
|
+
mode: "search",
|
|
114
|
+
source: command.source,
|
|
115
|
+
model: { id: command.model },
|
|
116
|
+
patterns: patternIds,
|
|
117
|
+
stats: {
|
|
118
|
+
elapsedMs: Date.now() - startedAt,
|
|
119
|
+
modelCalls: 0,
|
|
120
|
+
skipped: true,
|
|
121
|
+
skipReason: "no_candidates",
|
|
122
|
+
filesChanged: changeSet.summary.fileCount,
|
|
123
|
+
entitiesScanned: changeSet.summary.total,
|
|
124
|
+
candidates: contexts.length,
|
|
125
|
+
searchTargets: 0,
|
|
126
|
+
repomixFiles: initialPack.filePaths.length,
|
|
127
|
+
repomixTokens: initialPack.totalTokens,
|
|
128
|
+
repomixConfig: initialPack.config,
|
|
129
|
+
profileId: profile?.id,
|
|
130
|
+
targetsByPattern,
|
|
131
|
+
targetsPreview,
|
|
157
132
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
133
|
+
matches: [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
137
|
+
? initialPack
|
|
138
|
+
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
139
|
+
const modelPath = await firstRunModelBootstrap(command.model);
|
|
140
|
+
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
141
|
+
const request = buildSearchRequest(changeSet, searchContexts, pack, checks, profile, command.includeCounterReasonInPrompt);
|
|
142
|
+
const inputTokens = await countPromptTokens(model, request.prompt);
|
|
143
|
+
if (inputTokens > maxSearchInputTokens) {
|
|
144
|
+
return {
|
|
145
|
+
schemaVersion: "search.v1",
|
|
146
|
+
mode: "search",
|
|
147
|
+
source: command.source,
|
|
148
|
+
model: { id: command.model },
|
|
149
|
+
patterns: patternIds,
|
|
150
|
+
stats: {
|
|
151
|
+
elapsedMs: Date.now() - startedAt,
|
|
152
|
+
modelCalls: 0,
|
|
153
|
+
inputTokens,
|
|
154
|
+
inputTokenCap: maxSearchInputTokens,
|
|
155
|
+
skipped: true,
|
|
156
|
+
skipReason: "input_too_large",
|
|
157
|
+
filesChanged: changeSet.summary.fileCount,
|
|
158
|
+
entitiesScanned: changeSet.summary.total,
|
|
159
|
+
candidates: contexts.length,
|
|
160
|
+
searchTargets: searchContexts.length,
|
|
161
|
+
repomixFiles: pack.filePaths.length,
|
|
162
|
+
repomixTokens: pack.totalTokens,
|
|
163
|
+
repomixConfig: pack.config,
|
|
164
|
+
profileId: profile?.id,
|
|
165
|
+
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
166
|
+
targetsPreview: previewTargets(searchContexts),
|
|
167
|
+
},
|
|
168
|
+
matches: [],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const { value: matches } = await t.trace("search.model", () => runSearch(model, request), { count: (v) => v.length });
|
|
172
|
+
return {
|
|
173
|
+
schemaVersion: "search.v1",
|
|
174
|
+
mode: "search",
|
|
175
|
+
source: command.source,
|
|
176
|
+
model: { id: command.model },
|
|
177
|
+
patterns: patternIds,
|
|
178
|
+
stats: {
|
|
179
|
+
elapsedMs: Date.now() - startedAt,
|
|
180
|
+
modelCalls: 1,
|
|
181
|
+
inputTokens,
|
|
182
|
+
inputTokenCap: maxSearchInputTokens,
|
|
183
|
+
filesChanged: changeSet.summary.fileCount,
|
|
184
|
+
entitiesScanned: changeSet.summary.total,
|
|
185
|
+
candidates: contexts.length,
|
|
186
|
+
searchTargets: searchContexts.length,
|
|
187
|
+
repomixFiles: pack.filePaths.length,
|
|
188
|
+
repomixTokens: pack.totalTokens,
|
|
189
|
+
repomixConfig: pack.config,
|
|
190
|
+
profileId: profile?.id,
|
|
191
|
+
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
192
|
+
targetsPreview: previewTargets(searchContexts),
|
|
162
193
|
},
|
|
163
|
-
|
|
194
|
+
matches,
|
|
164
195
|
};
|
|
165
196
|
}
|
|
166
197
|
finally {
|
|
167
198
|
await changeSet.cleanup();
|
|
168
199
|
}
|
|
169
200
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
stats.finding += result.stats.finding;
|
|
179
|
-
stats.clean += result.stats.clean;
|
|
180
|
-
stats.uncertain += result.stats.uncertain;
|
|
181
|
-
stats.invalid += result.stats.invalid;
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
findings,
|
|
185
|
-
summary: findings.length === 0
|
|
186
|
-
? "No clear judgment-offload signal found."
|
|
187
|
-
: `${findings.length} finding review${findings.length === 1 ? "" : "s"} accepted.`,
|
|
188
|
-
stats,
|
|
189
|
-
auditModelCalls: traceEvents.filter((event) => event.name === "audit.batch").length,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
async function findingsAuditBatch(model, changeSet, batch, checks, traceEvents, command, limiter, batchLabel) {
|
|
193
|
-
const { value: pack, ms: contextMs } = await trace.trace("context.pack", () => command.auditContext === "none"
|
|
194
|
-
? Promise.resolve(emptyContextPack())
|
|
195
|
-
: repomixContextPack(changeSet.contextCwd, batch, changeSet.changes), { fields: { candidates: batch.length } });
|
|
196
|
-
const request = findingsAuditRequest(changeSet, batch, pack, checks, command.auditPrompt);
|
|
197
|
-
const inputTokens = await countPromptTokens(model, request.prompt);
|
|
198
|
-
const contextEvent = {
|
|
199
|
-
name: "context.pack",
|
|
200
|
-
ms: contextMs,
|
|
201
|
-
count: pack.filePaths.length,
|
|
202
|
-
detail: `batch=${batchLabel} input_tokens=${inputTokens} pack_tokens=${pack.totalTokens} chars=${pack.totalCharacters}`,
|
|
203
|
-
};
|
|
204
|
-
traceEvents.push(contextEvent);
|
|
205
|
-
debugSemTrace(command, contextEvent);
|
|
206
|
-
if (inputTokens > command.maxAuditInputTokens) {
|
|
207
|
-
if (batch.length <= 1) {
|
|
208
|
-
throw new Error(`Findings audit input has ${inputTokens} tokens, above max ${command.maxAuditInputTokens}.`);
|
|
209
|
-
}
|
|
210
|
-
const splitAt = Math.ceil(batch.length / 2);
|
|
211
|
-
const splitEvent = {
|
|
212
|
-
name: "audit.split",
|
|
213
|
-
ms: 0,
|
|
214
|
-
count: batch.length,
|
|
215
|
-
detail: `batch=${batchLabel} input_tokens=${inputTokens} max=${command.maxAuditInputTokens}`,
|
|
216
|
-
};
|
|
217
|
-
traceEvents.push(splitEvent);
|
|
218
|
-
debugSemTrace(command, splitEvent);
|
|
219
|
-
const [left, right] = await Promise.all([
|
|
220
|
-
findingsAuditBatch(model, changeSet, batch.slice(0, splitAt), checks, traceEvents, command, limiter, `${batchLabel}.1`),
|
|
221
|
-
findingsAuditBatch(model, changeSet, batch.slice(splitAt), checks, traceEvents, command, limiter, `${batchLabel}.2`),
|
|
222
|
-
]);
|
|
223
|
-
return combineAuditResults(left, right);
|
|
224
|
-
}
|
|
225
|
-
const { value: result, ms: auditMs } = await trace.trace("audit.batch", () => limiter.run(() => runFindingsAudit(model, changeSet, batch, pack, checks, request)), { fields: { candidates: batch.length } });
|
|
226
|
-
const event = {
|
|
227
|
-
name: "audit.batch",
|
|
228
|
-
ms: auditMs,
|
|
229
|
-
count: result.findings.length,
|
|
230
|
-
detail: `batch=${batchLabel} candidates=${batch.length} input_tokens=${inputTokens} targets=${result.stats.totalTargets} clean=${result.stats.clean} uncertain=${result.stats.uncertain} invalid=${result.stats.invalid}`,
|
|
231
|
-
};
|
|
232
|
-
traceEvents.push(event);
|
|
233
|
-
debugSemTrace(command, event);
|
|
234
|
-
return result;
|
|
201
|
+
function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includeCounterReasonInPrompt) {
|
|
202
|
+
return searchRequest({
|
|
203
|
+
changeSet,
|
|
204
|
+
contexts,
|
|
205
|
+
pack,
|
|
206
|
+
patterns,
|
|
207
|
+
includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
|
|
208
|
+
});
|
|
235
209
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
stats: {
|
|
244
|
-
totalTargets: left.stats.totalTargets + right.stats.totalTargets,
|
|
245
|
-
finding: left.stats.finding + right.stats.finding,
|
|
246
|
-
clean: left.stats.clean + right.stats.clean,
|
|
247
|
-
uncertain: left.stats.uncertain + right.stats.uncertain,
|
|
248
|
-
invalid: left.stats.invalid + right.stats.invalid,
|
|
249
|
-
},
|
|
250
|
-
};
|
|
210
|
+
function printRunPlan(command, filesChanged, entitiesScanned, patternIds) {
|
|
211
|
+
if (command.json)
|
|
212
|
+
return;
|
|
213
|
+
console.error("🧙 stupify 🪄");
|
|
214
|
+
console.error(`Mode: search (${command.source})`);
|
|
215
|
+
console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
|
|
216
|
+
console.error(`Patterns: ${patternIds.join(", ")}`);
|
|
251
217
|
}
|
|
252
|
-
function
|
|
218
|
+
function countTargetsByPattern(contexts) {
|
|
253
219
|
const counts = {};
|
|
254
|
-
for (const
|
|
255
|
-
counts[
|
|
256
|
-
}
|
|
220
|
+
for (const context of contexts)
|
|
221
|
+
counts[context.checkId] = (counts[context.checkId] ?? 0) + 1;
|
|
257
222
|
return counts;
|
|
258
223
|
}
|
|
259
|
-
function
|
|
224
|
+
function previewTargets(contexts) {
|
|
260
225
|
return contexts.map((context) => ({
|
|
261
226
|
targetId: context.targetId,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
changeKind: context.changeKind,
|
|
266
|
-
scoutReason: context.reason,
|
|
267
|
-
sourceLabel,
|
|
227
|
+
patternId: context.checkId,
|
|
228
|
+
entityKind: context.entityKind || undefined,
|
|
229
|
+
sourceKind: context.filePath ? pathKind(context.filePath) : undefined,
|
|
268
230
|
}));
|
|
269
231
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
queue = [];
|
|
274
|
-
constructor(max) {
|
|
275
|
-
this.max = max;
|
|
276
|
-
}
|
|
277
|
-
async run(task) {
|
|
278
|
-
if (this.active >= this.max) {
|
|
279
|
-
await new Promise((resolve) => this.queue.push(resolve));
|
|
280
|
-
}
|
|
281
|
-
this.active += 1;
|
|
282
|
-
try {
|
|
283
|
-
return await task();
|
|
284
|
-
}
|
|
285
|
-
finally {
|
|
286
|
-
this.active -= 1;
|
|
287
|
-
this.queue.shift()?.();
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
async function scoutSemBatches(model, batches, checks, command, traceEvents, t) {
|
|
292
|
-
const candidates = [];
|
|
293
|
-
const seen = new Set();
|
|
294
|
-
const targetsByCheck = new Map();
|
|
295
|
-
const maxTargetsPerCheck = 6;
|
|
296
|
-
for (const [index, batch] of batches.entries()) {
|
|
297
|
-
if (candidates.length >= command.maxCandidates)
|
|
298
|
-
break;
|
|
299
|
-
const remaining = command.maxCandidates - candidates.length;
|
|
300
|
-
const { value: batchCandidates } = await t.trace("scout.batch", async () => scoutSemChanges(model, batch, checks, remaining), {
|
|
301
|
-
fields: { entities: batch.changes.length },
|
|
302
|
-
count: (v) => v.length,
|
|
303
|
-
detail: (v) => `batch=${index + 1}/${batches.length} entities=${batch.changes.length} remaining=${remaining}`,
|
|
304
|
-
});
|
|
305
|
-
for (const candidate of batchCandidates) {
|
|
306
|
-
const key = `${candidate.entityId}\u0000${candidate.checkId}`;
|
|
307
|
-
if (seen.has(key))
|
|
308
|
-
continue;
|
|
309
|
-
const checkCount = targetsByCheck.get(candidate.checkId) ?? 0;
|
|
310
|
-
if (checkCount >= maxTargetsPerCheck)
|
|
311
|
-
continue;
|
|
312
|
-
seen.add(key);
|
|
313
|
-
targetsByCheck.set(candidate.checkId, checkCount + 1);
|
|
314
|
-
candidates.push({
|
|
315
|
-
...candidate,
|
|
316
|
-
targetId: `t${String(candidates.length + 1).padStart(3, "0")}`,
|
|
317
|
-
});
|
|
318
|
-
if (candidates.length >= command.maxCandidates)
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return candidates;
|
|
323
|
-
}
|
|
324
|
-
function debugSemTrace(command, event) {
|
|
325
|
-
if (!command.debugSem)
|
|
326
|
-
return;
|
|
327
|
-
const parts = [`trace ${event.name}`, `${event.ms}ms`];
|
|
328
|
-
if (event.count !== undefined)
|
|
329
|
-
parts.push(`count=${event.count}`);
|
|
330
|
-
if (event.detail)
|
|
331
|
-
parts.push(event.detail);
|
|
332
|
-
console.error(parts.join(" "));
|
|
333
|
-
}
|
|
334
|
-
function chunkSemChangeSet(changeSet) {
|
|
335
|
-
const chunks = [];
|
|
336
|
-
for (let index = 0; index < changeSet.changes.length; index += SEM_SCOUT_CHUNK_SIZE) {
|
|
337
|
-
const changes = changeSet.changes.slice(index, index + SEM_SCOUT_CHUNK_SIZE);
|
|
338
|
-
chunks.push({
|
|
339
|
-
...changeSet,
|
|
340
|
-
label: `${changeSet.label} batch ${chunks.length + 1}`,
|
|
341
|
-
changes,
|
|
342
|
-
summary: {
|
|
343
|
-
...changeSet.summary,
|
|
344
|
-
fileCount: new Set(changes.map((change) => change.filePath)).size,
|
|
345
|
-
total: changes.length,
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
return chunks;
|
|
350
|
-
}
|
|
351
|
-
function chunkSemContexts(contexts, chunkSize) {
|
|
352
|
-
const chunks = [];
|
|
353
|
-
for (let index = 0; index < contexts.length; index += chunkSize) {
|
|
354
|
-
chunks.push(contexts.slice(index, index + chunkSize));
|
|
355
|
-
}
|
|
356
|
-
return chunks;
|
|
357
|
-
}
|
|
358
|
-
function printRunPlan(command, diff, checkIds) {
|
|
359
|
-
if (command.json)
|
|
360
|
-
return;
|
|
361
|
-
console.error("🧙 stupify 🪄");
|
|
362
|
-
console.error(`Window: ${diff.label}`);
|
|
363
|
-
console.error(`Diff: ${diff.stats.filesChanged} files changed, ${diff.stats.additions} added, ${diff.stats.deletions} deleted`);
|
|
364
|
-
console.error(`Model: ${MODEL_REGISTRY[command.model].name}`);
|
|
365
|
-
console.error(`Checks: ${checkIds.join(", ")}`);
|
|
366
|
-
}
|
|
367
|
-
function printSemRunPlan(command, changeSet, checkIds) {
|
|
368
|
-
if (command.json)
|
|
369
|
-
return;
|
|
370
|
-
console.error("🧙 stupify 🪄");
|
|
371
|
-
console.error(`Window: ${changeSet.label}`);
|
|
372
|
-
console.error(`Sem: ${changeSet.summary.fileCount} files, ${changeSet.summary.total} changed entities`);
|
|
373
|
-
console.error(`Model: ${MODEL_REGISTRY[command.model].name}`);
|
|
374
|
-
console.error(`Checks: ${checkIds.join(", ")}`);
|
|
375
|
-
}
|
|
376
|
-
async function netDiffForCommand(command) {
|
|
377
|
-
if (command.kind === "since")
|
|
378
|
-
return netDiffSince(command.since);
|
|
379
|
-
if (command.kind === "stdin")
|
|
380
|
-
return netDiffFromStdin(await readDiffFromStdin());
|
|
381
|
-
if (command.kind === "commit")
|
|
382
|
-
return netDiffForCommit(command.commit);
|
|
383
|
-
return netDiffForRecentCommits(command.count);
|
|
232
|
+
function pathKind(filePath) {
|
|
233
|
+
const ext = filePath.split(".").pop();
|
|
234
|
+
return ext && ext !== filePath ? ext : "unknown";
|
|
384
235
|
}
|
|
385
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
236
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
386
237
|
process.exitCode = await main();
|
|
387
238
|
}
|