@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/stupify.ts
CHANGED
|
@@ -1,48 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
countPromptTokens,
|
|
7
|
-
findingsAuditRequest,
|
|
8
|
-
runFindingsAudit,
|
|
9
|
-
scoutBatch,
|
|
10
|
-
scoutSemChanges,
|
|
11
|
-
} from "./analysis.ts";
|
|
12
|
-
import { batchDiff } from "./batcher.ts";
|
|
13
|
-
import { candidateContexts } from "./candidate-context.ts";
|
|
14
|
-
import { enabledChecks } from "./checks.ts";
|
|
4
|
+
import { countPromptTokens, runSearch, searchRequest } from "./analysis.ts";
|
|
5
|
+
import { searchChecks } from "./checks.ts";
|
|
15
6
|
import { parseCommand } from "./command.ts";
|
|
16
|
-
import { MODEL_REGISTRY } from "./constants.ts";
|
|
17
7
|
import { counterScoutTargets } from "./counter-scout.ts";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
8
|
+
import { runDoctor } from "./doctor.ts";
|
|
9
|
+
import { runHookCommand } from "./hooks.ts";
|
|
10
|
+
import { firstRunModelBootstrap, loadLocalModel } from "./model.ts";
|
|
11
|
+
import { entityContextsFromChanges, emptyContextPack, repomixContextPack, repomixSearchConfig } from "./repomix-provider.ts";
|
|
12
|
+
import { helpText, renderSearchRun } from "./render.ts";
|
|
20
13
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import { emptyContextPack, entityContextsFromChanges, repomixContextPack } from "./repomix-provider.ts";
|
|
28
|
-
import { helpText, renderReport } from "./render.ts";
|
|
14
|
+
effectiveMaxCandidates,
|
|
15
|
+
effectiveMaxSearchInputTokens,
|
|
16
|
+
effectiveRepomixConfig,
|
|
17
|
+
effectiveSearchChecks,
|
|
18
|
+
loadSearchProfile,
|
|
19
|
+
} from "./search-profile.ts";
|
|
29
20
|
import { semChangeSetForCommand } from "./sem-provider.ts";
|
|
30
|
-
import { createTracer
|
|
31
|
-
import type {
|
|
32
|
-
AnalysisReport,
|
|
33
|
-
AnalyzeCommand,
|
|
34
|
-
AuditReviewResult,
|
|
35
|
-
AuditReviewStats,
|
|
36
|
-
DebugTarget,
|
|
37
|
-
FindingsResult,
|
|
38
|
-
NetDiff,
|
|
39
|
-
SemCandidate,
|
|
40
|
-
SemChangeSet,
|
|
41
|
-
SemContext,
|
|
42
|
-
TraceEvent,
|
|
43
|
-
} from "./types.ts";
|
|
44
|
-
|
|
45
|
-
const SEM_SCOUT_CHUNK_SIZE = 200;
|
|
21
|
+
import { createTracer } from "./trace.ts";
|
|
22
|
+
import type { SearchCommand, SearchProfile, SearchRunJson, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
|
|
46
23
|
|
|
47
24
|
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
48
25
|
const startedAt = Date.now();
|
|
@@ -52,19 +29,23 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
52
29
|
console.log(helpText());
|
|
53
30
|
return 0;
|
|
54
31
|
}
|
|
55
|
-
if (command.kind === "
|
|
56
|
-
|
|
57
|
-
|
|
32
|
+
if (command.kind === "hook") {
|
|
33
|
+
console.log(await runHookCommand(command.action));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (command.kind === "doctor") {
|
|
37
|
+
const result = await runDoctor();
|
|
38
|
+
console.log(result.text);
|
|
39
|
+
return result.exitCode;
|
|
40
|
+
}
|
|
41
|
+
if (command.kind === "bench-search") {
|
|
42
|
+
const { runSearchBench } = await import("./search-bench.ts");
|
|
43
|
+
console.log(await runSearchBench(command.configPath));
|
|
58
44
|
return 0;
|
|
59
45
|
}
|
|
60
46
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
command.engine === "sem"
|
|
64
|
-
? await runSemEngine(command, checks, startedAt)
|
|
65
|
-
: await runRawDiffEngine(command, checks, startedAt);
|
|
66
|
-
|
|
67
|
-
console.log(renderReport(report, command));
|
|
47
|
+
const run = await runSearchCommand(command, startedAt);
|
|
48
|
+
console.log(renderSearchRun(run, command));
|
|
68
49
|
return 0;
|
|
69
50
|
} catch (error) {
|
|
70
51
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -72,97 +53,23 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
72
53
|
}
|
|
73
54
|
}
|
|
74
55
|
|
|
75
|
-
async function
|
|
76
|
-
command: AnalyzeCommand,
|
|
77
|
-
checks: ReturnType<typeof enabledChecks>,
|
|
78
|
-
startedAt: number,
|
|
79
|
-
): Promise<AnalysisReport> {
|
|
80
|
-
const { value: diff, ms: diffMs } = await trace.trace("net.diff", () =>
|
|
81
|
-
netDiffForCommand(command),
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
printRunPlan(
|
|
85
|
-
command,
|
|
86
|
-
diff,
|
|
87
|
-
checks.map((check) => check.id),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const { value: models, ms: modelMs } = await trace.trace(
|
|
91
|
-
"model.load",
|
|
92
|
-
() => loadLocalModels(command.model),
|
|
93
|
-
);
|
|
94
|
-
const { scoutModel, auditModel } = models;
|
|
95
|
-
|
|
96
|
-
const batches = batchDiff(diff.text);
|
|
97
|
-
const { value: candidatePointers, ms: searchMs } = await trace.trace(
|
|
98
|
-
"search.total",
|
|
99
|
-
async () => {
|
|
100
|
-
const pointers: string[] = [];
|
|
101
|
-
for (const batch of batches) {
|
|
102
|
-
const { value: candidates } = await trace.trace(
|
|
103
|
-
"search.batch",
|
|
104
|
-
() => scoutBatch(scoutModel, batch, checks, diff.label),
|
|
105
|
-
{ fields: { batch: batch.id } },
|
|
106
|
-
);
|
|
107
|
-
pointers.push(...candidates);
|
|
108
|
-
}
|
|
109
|
-
return pointers;
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const contexts = candidateContexts(batches, candidatePointers);
|
|
114
|
-
const auditedContexts = contexts;
|
|
115
|
-
const { value: result, ms: auditMs } = await trace.trace(
|
|
116
|
-
"audit.candidates",
|
|
117
|
-
() => auditCandidates(auditModel, diff, auditedContexts, checks),
|
|
118
|
-
{ fields: { candidates: auditedContexts.length } },
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
run: {
|
|
123
|
-
mode: command.kind,
|
|
124
|
-
engine: command.engine,
|
|
125
|
-
auditContext: command.auditContext,
|
|
126
|
-
auditPrompt: command.auditPrompt,
|
|
127
|
-
modelId: command.model,
|
|
128
|
-
checkIds: checks.map((check) => check.id),
|
|
129
|
-
sourceId: diff.id,
|
|
130
|
-
label: diff.label,
|
|
131
|
-
stats: diff.stats,
|
|
132
|
-
batchesScanned: batches.length,
|
|
133
|
-
candidateCount: new Set(candidatePointers).size,
|
|
134
|
-
entitiesScanned: 0,
|
|
135
|
-
auditedCandidateCount: auditedContexts.length,
|
|
136
|
-
scoutModelCalls: batches.length,
|
|
137
|
-
auditModelCalls: auditedContexts.length > 0 ? 1 : 0,
|
|
138
|
-
warnings: [],
|
|
139
|
-
timingsMs: {
|
|
140
|
-
diff: diffMs,
|
|
141
|
-
modelLoad: modelMs,
|
|
142
|
-
search: searchMs,
|
|
143
|
-
audit: auditMs,
|
|
144
|
-
total: Date.now() - startedAt,
|
|
145
|
-
},
|
|
146
|
-
debugTargets: command.debugTargets ? [] : undefined,
|
|
147
|
-
},
|
|
148
|
-
result,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function runSemEngine(
|
|
153
|
-
command: AnalyzeCommand,
|
|
154
|
-
checks: ReturnType<typeof enabledChecks>,
|
|
155
|
-
startedAt: number,
|
|
156
|
-
): Promise<AnalysisReport> {
|
|
157
|
-
const traceEvents: TraceEvent[] = [];
|
|
56
|
+
export async function runSearchCommand(command: SearchCommand, startedAt: number): Promise<SearchRunJson> {
|
|
158
57
|
const t = createTracer({
|
|
58
|
+
writeLine: () => undefined,
|
|
159
59
|
onEvent: (event) => {
|
|
160
|
-
|
|
161
|
-
|
|
60
|
+
const parts = [`trace ${event.name}`, `${event.ms}ms`];
|
|
61
|
+
if (event.count !== undefined) parts.push(`count=${event.count}`);
|
|
62
|
+
if (event.detail) parts.push(event.detail);
|
|
63
|
+
console.error(parts.join(" "));
|
|
162
64
|
},
|
|
163
65
|
});
|
|
164
66
|
|
|
165
|
-
const
|
|
67
|
+
const profile = await loadSearchProfile(command.searchProfilePath);
|
|
68
|
+
const checks = profile ? effectiveSearchChecks(command.checkIds, profile) : searchChecks(command.checkIds);
|
|
69
|
+
const patternIds = checks.map((check) => check.id);
|
|
70
|
+
const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
|
|
71
|
+
const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
|
|
72
|
+
const { value: changeSet } = await t.trace(
|
|
166
73
|
"entity.diff",
|
|
167
74
|
() => semChangeSetForCommand(command),
|
|
168
75
|
{
|
|
@@ -171,426 +78,206 @@ async function runSemEngine(
|
|
|
171
78
|
},
|
|
172
79
|
);
|
|
173
80
|
|
|
174
|
-
printSemRunPlan(
|
|
175
|
-
command,
|
|
176
|
-
changeSet,
|
|
177
|
-
checks.map((check) => check.id),
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
const { value: models, ms: modelMs } = await t.trace(
|
|
181
|
-
"model.load",
|
|
182
|
-
() => loadLocalModels(command.model),
|
|
183
|
-
{
|
|
184
|
-
count: () => 2,
|
|
185
|
-
detail: () => "scout+audit",
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
|
-
const { scoutModel, auditModel } = models;
|
|
189
|
-
|
|
190
81
|
try {
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
82
|
+
printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
|
|
83
|
+
const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
|
|
84
|
+
const contexts = entityContextsFromChanges(candidates, changeSet.changes);
|
|
85
|
+
const targetsByPattern = countTargetsByPattern(contexts);
|
|
86
|
+
const targetsPreview = previewTargets(contexts);
|
|
87
|
+
if (contexts.length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
schemaVersion: "search.v1",
|
|
90
|
+
mode: "search",
|
|
91
|
+
source: command.source,
|
|
92
|
+
model: { id: command.model },
|
|
93
|
+
patterns: patternIds,
|
|
94
|
+
stats: {
|
|
95
|
+
elapsedMs: Date.now() - startedAt,
|
|
96
|
+
modelCalls: 0,
|
|
97
|
+
skipped: true,
|
|
98
|
+
skipReason: "no_candidates",
|
|
99
|
+
filesChanged: changeSet.summary.fileCount,
|
|
100
|
+
entitiesScanned: changeSet.summary.total,
|
|
101
|
+
candidates: 0,
|
|
102
|
+
searchTargets: 0,
|
|
103
|
+
repomixFiles: 0,
|
|
104
|
+
repomixTokens: 0,
|
|
105
|
+
profileId: profile?.id,
|
|
106
|
+
targetsByPattern,
|
|
107
|
+
targetsPreview,
|
|
108
|
+
},
|
|
109
|
+
matches: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
212
112
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
113
|
+
const baseRepomixConfig = effectiveRepomixConfig(repomixSearchConfig(), profile);
|
|
114
|
+
const initialPack = profile?.context === "sem"
|
|
115
|
+
? emptyContextPack()
|
|
116
|
+
: await t.trace(
|
|
117
|
+
"context.pack",
|
|
118
|
+
() => repomixContextPack(changeSet.contextCwd, contexts, changeSet.changes, baseRepomixConfig),
|
|
119
|
+
{
|
|
120
|
+
count: (v) => v.filePaths.length,
|
|
121
|
+
detail: (v) => `${v.totalTokens} tokens`,
|
|
122
|
+
},
|
|
123
|
+
).then((result) => result.value);
|
|
124
|
+
const packedFiles = new Set(initialPack.filePaths);
|
|
125
|
+
const searchContexts = profile?.context === "sem"
|
|
126
|
+
? contexts
|
|
127
|
+
: contexts.filter((context) => context.filePath && packedFiles.has(context.filePath));
|
|
128
|
+
if (searchContexts.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
schemaVersion: "search.v1",
|
|
131
|
+
mode: "search",
|
|
132
|
+
source: command.source,
|
|
133
|
+
model: { id: command.model },
|
|
134
|
+
patterns: patternIds,
|
|
135
|
+
stats: {
|
|
136
|
+
elapsedMs: Date.now() - startedAt,
|
|
137
|
+
modelCalls: 0,
|
|
138
|
+
skipped: true,
|
|
139
|
+
skipReason: "no_candidates",
|
|
140
|
+
filesChanged: changeSet.summary.fileCount,
|
|
141
|
+
entitiesScanned: changeSet.summary.total,
|
|
142
|
+
candidates: contexts.length,
|
|
143
|
+
searchTargets: 0,
|
|
144
|
+
repomixFiles: initialPack.filePaths.length,
|
|
145
|
+
repomixTokens: initialPack.totalTokens,
|
|
146
|
+
repomixConfig: initialPack.config,
|
|
147
|
+
profileId: profile?.id,
|
|
148
|
+
targetsByPattern,
|
|
149
|
+
targetsPreview,
|
|
150
|
+
},
|
|
151
|
+
matches: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
155
|
+
? initialPack
|
|
156
|
+
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
222
157
|
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
traceEvents,
|
|
233
|
-
t,
|
|
234
|
-
command,
|
|
235
|
-
),
|
|
236
|
-
{
|
|
237
|
-
count: (v) => v.findings.length,
|
|
238
|
-
detail: (v) =>
|
|
239
|
-
`${auditBatches.length} batches targets=${v.stats.totalTargets} clean=${v.stats.clean} uncertain=${v.stats.uncertain} invalid=${v.stats.invalid}`,
|
|
240
|
-
},
|
|
158
|
+
const modelPath = await firstRunModelBootstrap(command.model);
|
|
159
|
+
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
160
|
+
const request = buildSearchRequest(
|
|
161
|
+
changeSet,
|
|
162
|
+
searchContexts,
|
|
163
|
+
pack,
|
|
164
|
+
checks,
|
|
165
|
+
profile,
|
|
166
|
+
command.includeCounterReasonInPrompt,
|
|
241
167
|
);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
checkIds: checks.map((check) => check.id),
|
|
251
|
-
sourceId: changeSet.id,
|
|
252
|
-
label: changeSet.label,
|
|
168
|
+
const inputTokens = await countPromptTokens(model, request.prompt);
|
|
169
|
+
if (inputTokens > maxSearchInputTokens) {
|
|
170
|
+
return {
|
|
171
|
+
schemaVersion: "search.v1",
|
|
172
|
+
mode: "search",
|
|
173
|
+
source: command.source,
|
|
174
|
+
model: { id: command.model },
|
|
175
|
+
patterns: patternIds,
|
|
253
176
|
stats: {
|
|
177
|
+
elapsedMs: Date.now() - startedAt,
|
|
178
|
+
modelCalls: 0,
|
|
179
|
+
inputTokens,
|
|
180
|
+
inputTokenCap: maxSearchInputTokens,
|
|
181
|
+
skipped: true,
|
|
182
|
+
skipReason: "input_too_large",
|
|
254
183
|
filesChanged: changeSet.summary.fileCount,
|
|
255
|
-
|
|
256
|
-
|
|
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),
|
|
257
193
|
},
|
|
258
|
-
|
|
194
|
+
matches: [],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { value: matches } = await t.trace(
|
|
199
|
+
"search.model",
|
|
200
|
+
() => runSearch(model, request),
|
|
201
|
+
{ count: (v) => v.length },
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
schemaVersion: "search.v1",
|
|
206
|
+
mode: "search",
|
|
207
|
+
source: command.source,
|
|
208
|
+
model: { id: command.model },
|
|
209
|
+
patterns: patternIds,
|
|
210
|
+
stats: {
|
|
211
|
+
elapsedMs: Date.now() - startedAt,
|
|
212
|
+
modelCalls: 1,
|
|
213
|
+
inputTokens,
|
|
214
|
+
inputTokenCap: maxSearchInputTokens,
|
|
215
|
+
filesChanged: changeSet.summary.fileCount,
|
|
259
216
|
entitiesScanned: changeSet.summary.total,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
search: searchMs,
|
|
269
|
-
audit: auditMs + contextMs,
|
|
270
|
-
total: Date.now() - startedAt,
|
|
271
|
-
},
|
|
272
|
-
warnings: [],
|
|
273
|
-
auditStats: result.stats,
|
|
274
|
-
debugTargets: command.debugTargets ? debugTargetsFromContexts(contexts, changeSet.label) : undefined,
|
|
275
|
-
traceEvents,
|
|
217
|
+
candidates: contexts.length,
|
|
218
|
+
searchTargets: searchContexts.length,
|
|
219
|
+
repomixFiles: pack.filePaths.length,
|
|
220
|
+
repomixTokens: pack.totalTokens,
|
|
221
|
+
repomixConfig: pack.config,
|
|
222
|
+
profileId: profile?.id,
|
|
223
|
+
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
224
|
+
targetsPreview: previewTargets(searchContexts),
|
|
276
225
|
},
|
|
277
|
-
|
|
226
|
+
matches,
|
|
278
227
|
};
|
|
279
228
|
} finally {
|
|
280
229
|
await changeSet.cleanup();
|
|
281
230
|
}
|
|
282
231
|
}
|
|
283
232
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
changeSet,
|
|
300
|
-
batch,
|
|
301
|
-
checks,
|
|
302
|
-
traceEvents,
|
|
303
|
-
command,
|
|
304
|
-
limiter,
|
|
305
|
-
`${index + 1}/${batches.length}`,
|
|
306
|
-
);
|
|
307
|
-
findings.push(...result.findings);
|
|
308
|
-
stats.totalTargets += result.stats.totalTargets;
|
|
309
|
-
stats.finding += result.stats.finding;
|
|
310
|
-
stats.clean += result.stats.clean;
|
|
311
|
-
stats.uncertain += result.stats.uncertain;
|
|
312
|
-
stats.invalid += result.stats.invalid;
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
findings,
|
|
316
|
-
summary:
|
|
317
|
-
findings.length === 0
|
|
318
|
-
? "No clear judgment-offload signal found."
|
|
319
|
-
: `${findings.length} finding review${findings.length === 1 ? "" : "s"} accepted.`,
|
|
320
|
-
stats,
|
|
321
|
-
auditModelCalls: traceEvents.filter((event) => event.name === "audit.batch").length,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function findingsAuditBatch(
|
|
326
|
-
model: LocalModel,
|
|
327
|
-
changeSet: SemChangeSet,
|
|
328
|
-
batch: readonly SemContext[],
|
|
329
|
-
checks: ReturnType<typeof enabledChecks>,
|
|
330
|
-
traceEvents: TraceEvent[],
|
|
331
|
-
command: AnalyzeCommand,
|
|
332
|
-
limiter: ConcurrencyLimiter,
|
|
333
|
-
batchLabel: string,
|
|
334
|
-
): Promise<AuditReviewResult> {
|
|
335
|
-
const { value: pack, ms: contextMs } = await trace.trace(
|
|
336
|
-
"context.pack",
|
|
337
|
-
() =>
|
|
338
|
-
command.auditContext === "none"
|
|
339
|
-
? Promise.resolve(emptyContextPack())
|
|
340
|
-
: repomixContextPack(changeSet.contextCwd, batch, changeSet.changes),
|
|
341
|
-
{ fields: { candidates: batch.length } },
|
|
342
|
-
);
|
|
343
|
-
const request = findingsAuditRequest(changeSet, batch, pack, checks, command.auditPrompt);
|
|
344
|
-
const inputTokens = await countPromptTokens(model, request.prompt);
|
|
345
|
-
const contextEvent = {
|
|
346
|
-
name: "context.pack",
|
|
347
|
-
ms: contextMs,
|
|
348
|
-
count: pack.filePaths.length,
|
|
349
|
-
detail: `batch=${batchLabel} input_tokens=${inputTokens} pack_tokens=${pack.totalTokens} chars=${pack.totalCharacters}`,
|
|
350
|
-
};
|
|
351
|
-
traceEvents.push(contextEvent);
|
|
352
|
-
debugSemTrace(command, contextEvent);
|
|
353
|
-
|
|
354
|
-
if (inputTokens > command.maxAuditInputTokens) {
|
|
355
|
-
if (batch.length <= 1) {
|
|
356
|
-
throw new Error(`Findings audit input has ${inputTokens} tokens, above max ${command.maxAuditInputTokens}.`);
|
|
357
|
-
}
|
|
358
|
-
const splitAt = Math.ceil(batch.length / 2);
|
|
359
|
-
const splitEvent = {
|
|
360
|
-
name: "audit.split",
|
|
361
|
-
ms: 0,
|
|
362
|
-
count: batch.length,
|
|
363
|
-
detail: `batch=${batchLabel} input_tokens=${inputTokens} max=${command.maxAuditInputTokens}`,
|
|
364
|
-
};
|
|
365
|
-
traceEvents.push(splitEvent);
|
|
366
|
-
debugSemTrace(command, splitEvent);
|
|
367
|
-
const [left, right] = await Promise.all([
|
|
368
|
-
findingsAuditBatch(
|
|
369
|
-
model,
|
|
370
|
-
changeSet,
|
|
371
|
-
batch.slice(0, splitAt),
|
|
372
|
-
checks,
|
|
373
|
-
traceEvents,
|
|
374
|
-
command,
|
|
375
|
-
limiter,
|
|
376
|
-
`${batchLabel}.1`,
|
|
377
|
-
),
|
|
378
|
-
findingsAuditBatch(
|
|
379
|
-
model,
|
|
380
|
-
changeSet,
|
|
381
|
-
batch.slice(splitAt),
|
|
382
|
-
checks,
|
|
383
|
-
traceEvents,
|
|
384
|
-
command,
|
|
385
|
-
limiter,
|
|
386
|
-
`${batchLabel}.2`,
|
|
387
|
-
),
|
|
388
|
-
]);
|
|
389
|
-
return combineAuditResults(left, right);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const { value: result, ms: auditMs } = await trace.trace(
|
|
393
|
-
"audit.batch",
|
|
394
|
-
() => limiter.run(() => runFindingsAudit(model, changeSet, batch, pack, checks, request)),
|
|
395
|
-
{ fields: { candidates: batch.length } },
|
|
396
|
-
);
|
|
397
|
-
const event = {
|
|
398
|
-
name: "audit.batch",
|
|
399
|
-
ms: auditMs,
|
|
400
|
-
count: result.findings.length,
|
|
401
|
-
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}`,
|
|
402
|
-
};
|
|
403
|
-
traceEvents.push(event);
|
|
404
|
-
debugSemTrace(command, event);
|
|
405
|
-
return result;
|
|
233
|
+
function buildSearchRequest(
|
|
234
|
+
changeSet: Parameters<typeof searchRequest>[0]["changeSet"],
|
|
235
|
+
contexts: Parameters<typeof searchRequest>[0]["contexts"],
|
|
236
|
+
pack: SemContextPack,
|
|
237
|
+
patterns: readonly StupifyCheck[],
|
|
238
|
+
profile: SearchProfile | null,
|
|
239
|
+
includeCounterReasonInPrompt: boolean,
|
|
240
|
+
) {
|
|
241
|
+
return searchRequest({
|
|
242
|
+
changeSet,
|
|
243
|
+
contexts,
|
|
244
|
+
pack,
|
|
245
|
+
patterns,
|
|
246
|
+
includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
|
|
247
|
+
});
|
|
406
248
|
}
|
|
407
249
|
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
stats: {
|
|
420
|
-
totalTargets: left.stats.totalTargets + right.stats.totalTargets,
|
|
421
|
-
finding: left.stats.finding + right.stats.finding,
|
|
422
|
-
clean: left.stats.clean + right.stats.clean,
|
|
423
|
-
uncertain: left.stats.uncertain + right.stats.uncertain,
|
|
424
|
-
invalid: left.stats.invalid + right.stats.invalid,
|
|
425
|
-
},
|
|
426
|
-
};
|
|
250
|
+
function printRunPlan(
|
|
251
|
+
command: SearchCommand,
|
|
252
|
+
filesChanged: number,
|
|
253
|
+
entitiesScanned: number,
|
|
254
|
+
patternIds: readonly string[],
|
|
255
|
+
): void {
|
|
256
|
+
if (command.json) return;
|
|
257
|
+
console.error("🧙 stupify 🪄");
|
|
258
|
+
console.error(`Mode: search (${command.source})`);
|
|
259
|
+
console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
|
|
260
|
+
console.error(`Patterns: ${patternIds.join(", ")}`);
|
|
427
261
|
}
|
|
428
262
|
|
|
429
|
-
function
|
|
263
|
+
function countTargetsByPattern(contexts: readonly SemContext[]): Record<string, number> {
|
|
430
264
|
const counts: Record<string, number> = {};
|
|
431
|
-
for (const
|
|
432
|
-
counts[candidate.checkId] = (counts[candidate.checkId] ?? 0) + 1;
|
|
433
|
-
}
|
|
265
|
+
for (const context of contexts) counts[context.checkId] = (counts[context.checkId] ?? 0) + 1;
|
|
434
266
|
return counts;
|
|
435
267
|
}
|
|
436
268
|
|
|
437
|
-
function
|
|
438
|
-
contexts: readonly SemContext[],
|
|
439
|
-
sourceLabel: string,
|
|
440
|
-
): readonly DebugTarget[] {
|
|
269
|
+
function previewTargets(contexts: readonly SemContext[]) {
|
|
441
270
|
return contexts.map((context) => ({
|
|
442
271
|
targetId: context.targetId,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
changeKind: context.changeKind,
|
|
447
|
-
scoutReason: context.reason,
|
|
448
|
-
sourceLabel,
|
|
272
|
+
patternId: context.checkId,
|
|
273
|
+
entityKind: context.entityKind || undefined,
|
|
274
|
+
sourceKind: context.filePath ? pathKind(context.filePath) : undefined,
|
|
449
275
|
}));
|
|
450
276
|
}
|
|
451
277
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
constructor(private readonly max: number) {}
|
|
457
|
-
|
|
458
|
-
async run<T>(task: () => Promise<T>): Promise<T> {
|
|
459
|
-
if (this.active >= this.max) {
|
|
460
|
-
await new Promise<void>((resolve) => this.queue.push(resolve));
|
|
461
|
-
}
|
|
462
|
-
this.active += 1;
|
|
463
|
-
try {
|
|
464
|
-
return await task();
|
|
465
|
-
} finally {
|
|
466
|
-
this.active -= 1;
|
|
467
|
-
this.queue.shift()?.();
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
async function scoutSemBatches(
|
|
473
|
-
model: LocalModel,
|
|
474
|
-
batches: readonly SemChangeSet[],
|
|
475
|
-
checks: ReturnType<typeof enabledChecks>,
|
|
476
|
-
command: AnalyzeCommand,
|
|
477
|
-
traceEvents: TraceEvent[],
|
|
478
|
-
t: ReturnType<typeof createTracer>,
|
|
479
|
-
): Promise<readonly SemCandidate[]> {
|
|
480
|
-
const candidates: SemCandidate[] = [];
|
|
481
|
-
const seen = new Set<string>();
|
|
482
|
-
const targetsByCheck = new Map<string, number>();
|
|
483
|
-
const maxTargetsPerCheck = 6;
|
|
484
|
-
for (const [index, batch] of batches.entries()) {
|
|
485
|
-
if (candidates.length >= command.maxCandidates) break;
|
|
486
|
-
const remaining: number = command.maxCandidates - candidates.length;
|
|
487
|
-
const { value: batchCandidates } = await t.trace(
|
|
488
|
-
"scout.batch",
|
|
489
|
-
async () => scoutSemChanges(model, batch, checks, remaining),
|
|
490
|
-
{
|
|
491
|
-
fields: { entities: batch.changes.length },
|
|
492
|
-
count: (v) => v.length,
|
|
493
|
-
detail: (v) =>
|
|
494
|
-
`batch=${index + 1}/${batches.length} entities=${batch.changes.length} remaining=${remaining}`,
|
|
495
|
-
},
|
|
496
|
-
);
|
|
497
|
-
for (const candidate of batchCandidates) {
|
|
498
|
-
const key = `${candidate.entityId}\u0000${candidate.checkId}`;
|
|
499
|
-
if (seen.has(key)) continue;
|
|
500
|
-
const checkCount = targetsByCheck.get(candidate.checkId) ?? 0;
|
|
501
|
-
if (checkCount >= maxTargetsPerCheck) continue;
|
|
502
|
-
seen.add(key);
|
|
503
|
-
targetsByCheck.set(candidate.checkId, checkCount + 1);
|
|
504
|
-
candidates.push({
|
|
505
|
-
...candidate,
|
|
506
|
-
targetId: `t${String(candidates.length + 1).padStart(3, "0")}`,
|
|
507
|
-
});
|
|
508
|
-
if (candidates.length >= command.maxCandidates) break;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return candidates;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function debugSemTrace(command: AnalyzeCommand, event: TraceEvent): void {
|
|
515
|
-
if (!command.debugSem) return;
|
|
516
|
-
const parts = [`trace ${event.name}`, `${event.ms}ms`];
|
|
517
|
-
if (event.count !== undefined) parts.push(`count=${event.count}`);
|
|
518
|
-
if (event.detail) parts.push(event.detail);
|
|
519
|
-
console.error(parts.join(" "));
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function chunkSemChangeSet(changeSet: SemChangeSet): readonly SemChangeSet[] {
|
|
523
|
-
const chunks: SemChangeSet[] = [];
|
|
524
|
-
for (
|
|
525
|
-
let index = 0;
|
|
526
|
-
index < changeSet.changes.length;
|
|
527
|
-
index += SEM_SCOUT_CHUNK_SIZE
|
|
528
|
-
) {
|
|
529
|
-
const changes = changeSet.changes.slice(
|
|
530
|
-
index,
|
|
531
|
-
index + SEM_SCOUT_CHUNK_SIZE,
|
|
532
|
-
);
|
|
533
|
-
chunks.push({
|
|
534
|
-
...changeSet,
|
|
535
|
-
label: `${changeSet.label} batch ${chunks.length + 1}`,
|
|
536
|
-
changes,
|
|
537
|
-
summary: {
|
|
538
|
-
...changeSet.summary,
|
|
539
|
-
fileCount: new Set(changes.map((change) => change.filePath)).size,
|
|
540
|
-
total: changes.length,
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
return chunks;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function chunkSemContexts(
|
|
548
|
-
contexts: readonly SemContext[],
|
|
549
|
-
chunkSize: number,
|
|
550
|
-
): readonly (readonly SemContext[])[] {
|
|
551
|
-
const chunks: SemContext[][] = [];
|
|
552
|
-
for (let index = 0; index < contexts.length; index += chunkSize) {
|
|
553
|
-
chunks.push(contexts.slice(index, index + chunkSize));
|
|
554
|
-
}
|
|
555
|
-
return chunks;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function printRunPlan(
|
|
559
|
-
command: AnalyzeCommand,
|
|
560
|
-
diff: NetDiff,
|
|
561
|
-
checkIds: readonly string[],
|
|
562
|
-
): void {
|
|
563
|
-
if (command.json) return;
|
|
564
|
-
console.error("🧙 stupify 🪄");
|
|
565
|
-
console.error(`Window: ${diff.label}`);
|
|
566
|
-
console.error(
|
|
567
|
-
`Diff: ${diff.stats.filesChanged} files changed, ${diff.stats.additions} added, ${diff.stats.deletions} deleted`,
|
|
568
|
-
);
|
|
569
|
-
console.error(`Model: ${MODEL_REGISTRY[command.model].name}`);
|
|
570
|
-
console.error(`Checks: ${checkIds.join(", ")}`);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function printSemRunPlan(
|
|
574
|
-
command: AnalyzeCommand,
|
|
575
|
-
changeSet: SemChangeSet,
|
|
576
|
-
checkIds: readonly string[],
|
|
577
|
-
): void {
|
|
578
|
-
if (command.json) return;
|
|
579
|
-
console.error("🧙 stupify 🪄");
|
|
580
|
-
console.error(`Window: ${changeSet.label}`);
|
|
581
|
-
console.error(
|
|
582
|
-
`Sem: ${changeSet.summary.fileCount} files, ${changeSet.summary.total} changed entities`,
|
|
583
|
-
);
|
|
584
|
-
console.error(`Model: ${MODEL_REGISTRY[command.model].name}`);
|
|
585
|
-
console.error(`Checks: ${checkIds.join(", ")}`);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async function netDiffForCommand(command: AnalyzeCommand): Promise<NetDiff> {
|
|
589
|
-
if (command.kind === "since") return netDiffSince(command.since);
|
|
590
|
-
if (command.kind === "stdin")
|
|
591
|
-
return netDiffFromStdin(await readDiffFromStdin());
|
|
592
|
-
if (command.kind === "commit") return netDiffForCommit(command.commit);
|
|
593
|
-
return netDiffForRecentCommits(command.count);
|
|
278
|
+
function pathKind(filePath: string): string {
|
|
279
|
+
const ext = filePath.split(".").pop();
|
|
280
|
+
return ext && ext !== filePath ? ext : "unknown";
|
|
594
281
|
}
|
|
595
282
|
|
|
596
283
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|