@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/src/stupify.ts
CHANGED
|
@@ -1,48 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
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";
|
|
5
|
+
import { countPromptTokens, runSearch, searchRequest } from "./analysis.ts";
|
|
6
|
+
import { searchChecks } from "./checks.ts";
|
|
15
7
|
import { parseCommand } from "./command.ts";
|
|
16
|
-
import { MODEL_REGISTRY } from "./constants.ts";
|
|
17
8
|
import { counterScoutTargets } from "./counter-scout.ts";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
9
|
+
import { runDoctor } from "./doctor.ts";
|
|
10
|
+
import { runHookCommand } from "./hooks.ts";
|
|
11
|
+
import { firstRunModelBootstrap, loadLocalModel } from "./model.ts";
|
|
12
|
+
import { entityContextsFromChanges, emptyContextPack, repomixContextPack, repomixSearchConfig } from "./repomix-provider.ts";
|
|
13
|
+
import { helpText, renderSearchRun } from "./render.ts";
|
|
20
14
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import { emptyContextPack, entityContextsFromChanges, repomixContextPack } from "./repomix-provider.ts";
|
|
28
|
-
import { helpText, renderReport } from "./render.ts";
|
|
15
|
+
effectiveMaxCandidates,
|
|
16
|
+
effectiveMaxSearchInputTokens,
|
|
17
|
+
effectiveRepomixConfig,
|
|
18
|
+
effectiveSearchChecks,
|
|
19
|
+
loadSearchProfile,
|
|
20
|
+
} from "./search-profile.ts";
|
|
29
21
|
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;
|
|
22
|
+
import { createTracer } from "./trace.ts";
|
|
23
|
+
import type { SearchCommand, SearchProfile, SearchRunJson, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
|
|
46
24
|
|
|
47
25
|
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
48
26
|
const startedAt = Date.now();
|
|
@@ -52,19 +30,23 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
52
30
|
console.log(helpText());
|
|
53
31
|
return 0;
|
|
54
32
|
}
|
|
55
|
-
if (command.kind === "
|
|
56
|
-
|
|
57
|
-
|
|
33
|
+
if (command.kind === "hook") {
|
|
34
|
+
console.log(await runHookCommand(command.action));
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (command.kind === "doctor") {
|
|
38
|
+
const result = await runDoctor();
|
|
39
|
+
console.log(result.text);
|
|
40
|
+
return result.exitCode;
|
|
41
|
+
}
|
|
42
|
+
if (command.kind === "bench-search") {
|
|
43
|
+
const { runSearchBench } = await import("./search-bench.ts");
|
|
44
|
+
console.log(await runSearchBench(command.configPath));
|
|
58
45
|
return 0;
|
|
59
46
|
}
|
|
60
47
|
|
|
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));
|
|
48
|
+
const run = await runSearchCommand(command, startedAt);
|
|
49
|
+
console.log(renderSearchRun(run, command));
|
|
68
50
|
return 0;
|
|
69
51
|
} catch (error) {
|
|
70
52
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -72,97 +54,23 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
72
54
|
}
|
|
73
55
|
}
|
|
74
56
|
|
|
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[] = [];
|
|
57
|
+
export async function runSearchCommand(command: SearchCommand, startedAt: number): Promise<SearchRunJson> {
|
|
158
58
|
const t = createTracer({
|
|
59
|
+
writeLine: () => undefined,
|
|
159
60
|
onEvent: (event) => {
|
|
160
|
-
|
|
161
|
-
|
|
61
|
+
const parts = [`trace ${event.name}`, `${event.ms}ms`];
|
|
62
|
+
if (event.count !== undefined) parts.push(`count=${event.count}`);
|
|
63
|
+
if (event.detail) parts.push(event.detail);
|
|
64
|
+
console.error(parts.join(" "));
|
|
162
65
|
},
|
|
163
66
|
});
|
|
164
67
|
|
|
165
|
-
const
|
|
68
|
+
const profile = await loadSearchProfile(command.searchProfilePath);
|
|
69
|
+
const checks = profile ? effectiveSearchChecks(command.checkIds, profile) : searchChecks(command.checkIds);
|
|
70
|
+
const patternIds = checks.map((check) => check.id);
|
|
71
|
+
const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
|
|
72
|
+
const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
|
|
73
|
+
const { value: changeSet } = await t.trace(
|
|
166
74
|
"entity.diff",
|
|
167
75
|
() => semChangeSetForCommand(command),
|
|
168
76
|
{
|
|
@@ -171,428 +79,208 @@ async function runSemEngine(
|
|
|
171
79
|
},
|
|
172
80
|
);
|
|
173
81
|
|
|
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
82
|
try {
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
83
|
+
printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
|
|
84
|
+
const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
|
|
85
|
+
const contexts = entityContextsFromChanges(candidates, changeSet.changes);
|
|
86
|
+
const targetsByPattern = countTargetsByPattern(contexts);
|
|
87
|
+
const targetsPreview = previewTargets(contexts);
|
|
88
|
+
if (contexts.length === 0) {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: "search.v1",
|
|
91
|
+
mode: "search",
|
|
92
|
+
source: command.source,
|
|
93
|
+
model: { id: command.model },
|
|
94
|
+
patterns: patternIds,
|
|
95
|
+
stats: {
|
|
96
|
+
elapsedMs: Date.now() - startedAt,
|
|
97
|
+
modelCalls: 0,
|
|
98
|
+
skipped: true,
|
|
99
|
+
skipReason: "no_candidates",
|
|
100
|
+
filesChanged: changeSet.summary.fileCount,
|
|
101
|
+
entitiesScanned: changeSet.summary.total,
|
|
102
|
+
candidates: 0,
|
|
103
|
+
searchTargets: 0,
|
|
104
|
+
repomixFiles: 0,
|
|
105
|
+
repomixTokens: 0,
|
|
106
|
+
profileId: profile?.id,
|
|
107
|
+
targetsByPattern,
|
|
108
|
+
targetsPreview,
|
|
109
|
+
},
|
|
110
|
+
matches: [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
212
113
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
114
|
+
const baseRepomixConfig = effectiveRepomixConfig(repomixSearchConfig(), profile);
|
|
115
|
+
const initialPack = profile?.context === "sem"
|
|
116
|
+
? emptyContextPack()
|
|
117
|
+
: await t.trace(
|
|
118
|
+
"context.pack",
|
|
119
|
+
() => repomixContextPack(changeSet.contextCwd, contexts, changeSet.changes, baseRepomixConfig),
|
|
120
|
+
{
|
|
121
|
+
count: (v) => v.filePaths.length,
|
|
122
|
+
detail: (v) => `${v.totalTokens} tokens`,
|
|
123
|
+
},
|
|
124
|
+
).then((result) => result.value);
|
|
125
|
+
const packedFiles = new Set(initialPack.filePaths);
|
|
126
|
+
const searchContexts = profile?.context === "sem"
|
|
127
|
+
? contexts
|
|
128
|
+
: contexts.filter((context) => context.filePath && packedFiles.has(context.filePath));
|
|
129
|
+
if (searchContexts.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
schemaVersion: "search.v1",
|
|
132
|
+
mode: "search",
|
|
133
|
+
source: command.source,
|
|
134
|
+
model: { id: command.model },
|
|
135
|
+
patterns: patternIds,
|
|
136
|
+
stats: {
|
|
137
|
+
elapsedMs: Date.now() - startedAt,
|
|
138
|
+
modelCalls: 0,
|
|
139
|
+
skipped: true,
|
|
140
|
+
skipReason: "no_candidates",
|
|
141
|
+
filesChanged: changeSet.summary.fileCount,
|
|
142
|
+
entitiesScanned: changeSet.summary.total,
|
|
143
|
+
candidates: contexts.length,
|
|
144
|
+
searchTargets: 0,
|
|
145
|
+
repomixFiles: initialPack.filePaths.length,
|
|
146
|
+
repomixTokens: initialPack.totalTokens,
|
|
147
|
+
repomixConfig: initialPack.config,
|
|
148
|
+
profileId: profile?.id,
|
|
149
|
+
targetsByPattern,
|
|
150
|
+
targetsPreview,
|
|
151
|
+
},
|
|
152
|
+
matches: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
156
|
+
? initialPack
|
|
157
|
+
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
222
158
|
|
|
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
|
-
},
|
|
159
|
+
const modelPath = await firstRunModelBootstrap(command.model);
|
|
160
|
+
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
161
|
+
const request = buildSearchRequest(
|
|
162
|
+
changeSet,
|
|
163
|
+
searchContexts,
|
|
164
|
+
pack,
|
|
165
|
+
checks,
|
|
166
|
+
profile,
|
|
167
|
+
command.includeCounterReasonInPrompt,
|
|
241
168
|
);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
checkIds: checks.map((check) => check.id),
|
|
251
|
-
sourceId: changeSet.id,
|
|
252
|
-
label: changeSet.label,
|
|
169
|
+
const inputTokens = await countPromptTokens(model, request.prompt);
|
|
170
|
+
if (inputTokens > maxSearchInputTokens) {
|
|
171
|
+
return {
|
|
172
|
+
schemaVersion: "search.v1",
|
|
173
|
+
mode: "search",
|
|
174
|
+
source: command.source,
|
|
175
|
+
model: { id: command.model },
|
|
176
|
+
patterns: patternIds,
|
|
253
177
|
stats: {
|
|
178
|
+
elapsedMs: Date.now() - startedAt,
|
|
179
|
+
modelCalls: 0,
|
|
180
|
+
inputTokens,
|
|
181
|
+
inputTokenCap: maxSearchInputTokens,
|
|
182
|
+
skipped: true,
|
|
183
|
+
skipReason: "input_too_large",
|
|
254
184
|
filesChanged: changeSet.summary.fileCount,
|
|
255
|
-
|
|
256
|
-
|
|
185
|
+
entitiesScanned: changeSet.summary.total,
|
|
186
|
+
candidates: contexts.length,
|
|
187
|
+
searchTargets: searchContexts.length,
|
|
188
|
+
repomixFiles: pack.filePaths.length,
|
|
189
|
+
repomixTokens: pack.totalTokens,
|
|
190
|
+
repomixConfig: pack.config,
|
|
191
|
+
profileId: profile?.id,
|
|
192
|
+
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
193
|
+
targetsPreview: previewTargets(searchContexts),
|
|
257
194
|
},
|
|
258
|
-
|
|
195
|
+
matches: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { value: matches } = await t.trace(
|
|
200
|
+
"search.model",
|
|
201
|
+
() => runSearch(model, request),
|
|
202
|
+
{ count: (v) => v.length },
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
schemaVersion: "search.v1",
|
|
207
|
+
mode: "search",
|
|
208
|
+
source: command.source,
|
|
209
|
+
model: { id: command.model },
|
|
210
|
+
patterns: patternIds,
|
|
211
|
+
stats: {
|
|
212
|
+
elapsedMs: Date.now() - startedAt,
|
|
213
|
+
modelCalls: 1,
|
|
214
|
+
inputTokens,
|
|
215
|
+
inputTokenCap: maxSearchInputTokens,
|
|
216
|
+
filesChanged: changeSet.summary.fileCount,
|
|
259
217
|
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,
|
|
218
|
+
candidates: contexts.length,
|
|
219
|
+
searchTargets: searchContexts.length,
|
|
220
|
+
repomixFiles: pack.filePaths.length,
|
|
221
|
+
repomixTokens: pack.totalTokens,
|
|
222
|
+
repomixConfig: pack.config,
|
|
223
|
+
profileId: profile?.id,
|
|
224
|
+
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
225
|
+
targetsPreview: previewTargets(searchContexts),
|
|
276
226
|
},
|
|
277
|
-
|
|
227
|
+
matches,
|
|
278
228
|
};
|
|
279
229
|
} finally {
|
|
280
230
|
await changeSet.cleanup();
|
|
281
231
|
}
|
|
282
232
|
}
|
|
283
233
|
|
|
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;
|
|
234
|
+
function buildSearchRequest(
|
|
235
|
+
changeSet: Parameters<typeof searchRequest>[0]["changeSet"],
|
|
236
|
+
contexts: Parameters<typeof searchRequest>[0]["contexts"],
|
|
237
|
+
pack: SemContextPack,
|
|
238
|
+
patterns: readonly StupifyCheck[],
|
|
239
|
+
profile: SearchProfile | null,
|
|
240
|
+
includeCounterReasonInPrompt: boolean,
|
|
241
|
+
) {
|
|
242
|
+
return searchRequest({
|
|
243
|
+
changeSet,
|
|
244
|
+
contexts,
|
|
245
|
+
pack,
|
|
246
|
+
patterns,
|
|
247
|
+
includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
|
|
248
|
+
});
|
|
406
249
|
}
|
|
407
250
|
|
|
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
|
-
};
|
|
251
|
+
function printRunPlan(
|
|
252
|
+
command: SearchCommand,
|
|
253
|
+
filesChanged: number,
|
|
254
|
+
entitiesScanned: number,
|
|
255
|
+
patternIds: readonly string[],
|
|
256
|
+
): void {
|
|
257
|
+
if (command.json) return;
|
|
258
|
+
console.error("🧙 stupify 🪄");
|
|
259
|
+
console.error(`Mode: search (${command.source})`);
|
|
260
|
+
console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
|
|
261
|
+
console.error(`Patterns: ${patternIds.join(", ")}`);
|
|
427
262
|
}
|
|
428
263
|
|
|
429
|
-
function
|
|
264
|
+
function countTargetsByPattern(contexts: readonly SemContext[]): Record<string, number> {
|
|
430
265
|
const counts: Record<string, number> = {};
|
|
431
|
-
for (const
|
|
432
|
-
counts[candidate.checkId] = (counts[candidate.checkId] ?? 0) + 1;
|
|
433
|
-
}
|
|
266
|
+
for (const context of contexts) counts[context.checkId] = (counts[context.checkId] ?? 0) + 1;
|
|
434
267
|
return counts;
|
|
435
268
|
}
|
|
436
269
|
|
|
437
|
-
function
|
|
438
|
-
contexts: readonly SemContext[],
|
|
439
|
-
sourceLabel: string,
|
|
440
|
-
): readonly DebugTarget[] {
|
|
270
|
+
function previewTargets(contexts: readonly SemContext[]) {
|
|
441
271
|
return contexts.map((context) => ({
|
|
442
272
|
targetId: context.targetId,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
changeKind: context.changeKind,
|
|
447
|
-
scoutReason: context.reason,
|
|
448
|
-
sourceLabel,
|
|
273
|
+
patternId: context.checkId,
|
|
274
|
+
entityKind: context.entityKind || undefined,
|
|
275
|
+
sourceKind: context.filePath ? pathKind(context.filePath) : undefined,
|
|
449
276
|
}));
|
|
450
277
|
}
|
|
451
278
|
|
|
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);
|
|
279
|
+
function pathKind(filePath: string): string {
|
|
280
|
+
const ext = filePath.split(".").pop();
|
|
281
|
+
return ext && ext !== filePath ? ext : "unknown";
|
|
594
282
|
}
|
|
595
283
|
|
|
596
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
284
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
597
285
|
process.exitCode = await main();
|
|
598
286
|
}
|