@stupify/cli 0.0.11 → 0.0.13
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/dist/analysis.js +1 -1
- package/dist/command.js +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/counter-scout.d.ts +7 -0
- package/dist/counter-scout.js +9 -1
- package/dist/git.js +3 -1
- package/dist/model.js +1 -1
- package/dist/render.js +32 -6
- package/dist/search-bench.js +4 -2
- package/dist/stupify.js +33 -3
- package/package.json +1 -1
- package/src/analysis.ts +1 -1
- package/src/command.ts +2 -2
- package/src/constants.ts +1 -1
- package/src/counter-scout.ts +21 -1
- package/src/git.ts +2 -1
- package/src/model.ts +1 -1
- package/src/render.ts +34 -6
- package/src/search-bench.ts +2 -2
- package/src/stupify.ts +38 -3
package/dist/analysis.js
CHANGED
|
@@ -101,7 +101,7 @@ function stringSnapshot(value) {
|
|
|
101
101
|
}
|
|
102
102
|
function limitSnapshot(value) {
|
|
103
103
|
const lines = value.split(/\r?\n/);
|
|
104
|
-
const limit =
|
|
104
|
+
const limit = 12;
|
|
105
105
|
if (lines.length <= limit)
|
|
106
106
|
return value;
|
|
107
107
|
return `${lines.slice(0, limit).join("\n")}
|
package/dist/command.js
CHANGED
|
@@ -3,7 +3,7 @@ const DEFAULT_SINCE = "2 weeks ago";
|
|
|
3
3
|
const DEFAULT_MAX_CANDIDATES = 10;
|
|
4
4
|
const DEFAULT_MAX_SEARCH_INPUT_TOKENS = 12_000;
|
|
5
5
|
export function parseCommand(argv) {
|
|
6
|
-
if (argv.length === 1 && isHelp(argv[0]))
|
|
6
|
+
if (argv.length === 1 && argv[0] && isHelp(argv[0]))
|
|
7
7
|
return { kind: "help" };
|
|
8
8
|
if (argv[0] === "bench") {
|
|
9
9
|
if (argv[1] !== "search" || !argv[2] || argv.length > 3) {
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
package/dist/counter-scout.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ type SignalBucket = Readonly<{
|
|
|
9
9
|
total: number;
|
|
10
10
|
examples: readonly Signal[];
|
|
11
11
|
}>;
|
|
12
|
+
export type CounterScoutPlan = Readonly<{
|
|
13
|
+
buckets: readonly SignalBucket[];
|
|
14
|
+
totalSignals: number;
|
|
15
|
+
maxTargets: number;
|
|
16
|
+
targets: readonly SemCandidate[];
|
|
17
|
+
}>;
|
|
12
18
|
export declare function counterScoutTargets(changeSet: SemChangeSet, checks: readonly StupifyCheck[], maxTargets: number): readonly SemCandidate[];
|
|
19
|
+
export declare function counterScoutPlan(changeSet: SemChangeSet, checks: readonly StupifyCheck[], maxTargets: number): CounterScoutPlan;
|
|
13
20
|
export declare function runSignalCounters(changeSet: SemChangeSet, checks: readonly StupifyCheck[]): readonly SignalBucket[];
|
|
14
21
|
export {};
|
package/dist/counter-scout.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const MAX_COUNTER_EXAMPLES_PER_CHECK = 4;
|
|
2
2
|
export function counterScoutTargets(changeSet, checks, maxTargets) {
|
|
3
|
+
return counterScoutPlan(changeSet, checks, maxTargets).targets;
|
|
4
|
+
}
|
|
5
|
+
export function counterScoutPlan(changeSet, checks, maxTargets) {
|
|
3
6
|
const buckets = runSignalCounters(changeSet, checks);
|
|
4
7
|
const targets = [];
|
|
5
8
|
let cursor = 0;
|
|
@@ -20,7 +23,12 @@ export function counterScoutTargets(changeSet, checks, maxTargets) {
|
|
|
20
23
|
}
|
|
21
24
|
cursor += 1;
|
|
22
25
|
}
|
|
23
|
-
return
|
|
26
|
+
return {
|
|
27
|
+
buckets,
|
|
28
|
+
totalSignals: buckets.reduce((sum, bucket) => sum + bucket.total, 0),
|
|
29
|
+
maxTargets,
|
|
30
|
+
targets,
|
|
31
|
+
};
|
|
24
32
|
}
|
|
25
33
|
export function runSignalCounters(changeSet, checks) {
|
|
26
34
|
return checks
|
package/dist/git.js
CHANGED
|
@@ -33,6 +33,8 @@ export async function sourceRangeForRecentCommits(count) {
|
|
|
33
33
|
throw new Error("No non-merge commits found.");
|
|
34
34
|
const oldest = commits[0];
|
|
35
35
|
const newest = commits[commits.length - 1];
|
|
36
|
+
if (!oldest || !newest)
|
|
37
|
+
throw new Error("Could not resolve recent commit range.");
|
|
36
38
|
const [base, target, shortBase, shortTarget] = await Promise.all([
|
|
37
39
|
revParse(`${oldest}^1`),
|
|
38
40
|
revParse(newest),
|
|
@@ -224,7 +226,7 @@ function statsFromDiff(diffText) {
|
|
|
224
226
|
let deletions = 0;
|
|
225
227
|
for (const line of diffText.split(/\r?\n/)) {
|
|
226
228
|
const fileMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
|
|
227
|
-
if (fileMatch)
|
|
229
|
+
if (fileMatch?.[1])
|
|
228
230
|
files.add(fileMatch[1]);
|
|
229
231
|
else if (line.startsWith("+") && !line.startsWith("+++"))
|
|
230
232
|
additions += 1;
|
package/dist/model.js
CHANGED
package/dist/render.js
CHANGED
|
@@ -25,9 +25,9 @@ ${format.label("Patterns:")} ${run.patterns.join(", ")}
|
|
|
25
25
|
${format.success("No judgment-offload signals found.")}`;
|
|
26
26
|
}
|
|
27
27
|
return `${slopHeading()}
|
|
28
|
-
${run.matches.map((match, index) => `${index + 1}. ${format.label(match.patternId)}
|
|
29
28
|
${committerLabel(run)} (${sourceLabel(command)})
|
|
30
29
|
|
|
30
|
+
${run.matches.map((match, index) => `${index + 1}. ${format.label(match.patternId)}
|
|
31
31
|
${match.reason}
|
|
32
32
|
|
|
33
33
|
\`\`\`
|
|
@@ -36,7 +36,7 @@ ${match.snapshot ?? match.proof}
|
|
|
36
36
|
${format.muted(match.proof)}
|
|
37
37
|
|
|
38
38
|
${match.checkWhy ?? "This pattern may indicate judgment-offload."}`).join("\n\n")}
|
|
39
|
-
${format.muted(
|
|
39
|
+
${format.muted(summaryLine(run))}`;
|
|
40
40
|
}
|
|
41
41
|
export function helpText() {
|
|
42
42
|
return `Stupify ${VERSION}
|
|
@@ -98,23 +98,32 @@ function sourceHint(command) {
|
|
|
98
98
|
}
|
|
99
99
|
function sourceLabel(command) {
|
|
100
100
|
if (command.kind === "staged")
|
|
101
|
-
return "staged
|
|
101
|
+
return "staged";
|
|
102
102
|
if (command.kind === "since")
|
|
103
|
-
return
|
|
103
|
+
return sinceLabel(command.since);
|
|
104
104
|
if (command.kind === "commit")
|
|
105
105
|
return `commit ${command.commit}`;
|
|
106
106
|
if (command.kind === "commits")
|
|
107
107
|
return `last ${command.count} commits`;
|
|
108
|
-
return "stdin
|
|
108
|
+
return "stdin";
|
|
109
109
|
}
|
|
110
110
|
function committerLabel(run) {
|
|
111
|
-
const committers = (run.stats.committers ?? []).
|
|
111
|
+
const committers = humanCommitters(run.stats.committers ?? []).map(committerDisplayName);
|
|
112
112
|
if (committers.length === 0)
|
|
113
113
|
return "unknown committer";
|
|
114
114
|
if (committers.length <= 3)
|
|
115
115
|
return committers.join(", ");
|
|
116
116
|
return `${committers.slice(0, 3).join(", ")} +${committers.length - 3} more`;
|
|
117
117
|
}
|
|
118
|
+
function humanCommitters(committers) {
|
|
119
|
+
const nonEmpty = committers.filter(Boolean);
|
|
120
|
+
const humans = nonEmpty.filter((committer) => !isBotCommitter(committer));
|
|
121
|
+
return humans.length > 0 ? humans : nonEmpty;
|
|
122
|
+
}
|
|
123
|
+
function isBotCommitter(value) {
|
|
124
|
+
return /(?:^|<)(?:github|dependabot|renovate)(?:\s|@|>)/i.test(value) ||
|
|
125
|
+
/(?:noreply@github\.com|bot@)/i.test(value);
|
|
126
|
+
}
|
|
118
127
|
function committerDisplayName(value) {
|
|
119
128
|
return value.replace(/\s*<[^>]+>\s*$/, "").trim() || value;
|
|
120
129
|
}
|
|
@@ -123,3 +132,20 @@ function slopHeading() {
|
|
|
123
132
|
return `${format.warn(format.heading(heading))}
|
|
124
133
|
${format.warn("=".repeat(heading.length))}`;
|
|
125
134
|
}
|
|
135
|
+
function sinceLabel(since) {
|
|
136
|
+
const value = since.trim().toLowerCase();
|
|
137
|
+
if (value === "yesterday" || value === "1 day ago")
|
|
138
|
+
return "yesterday";
|
|
139
|
+
const match = /^(\d+)\s+(day|week|month|year)s?\s+ago$/.exec(value);
|
|
140
|
+
if (!match)
|
|
141
|
+
return `since ${since}`;
|
|
142
|
+
const count = Number(match[1]);
|
|
143
|
+
const unit = match[2];
|
|
144
|
+
if (count === 1)
|
|
145
|
+
return `last ${unit}`;
|
|
146
|
+
return `last ${count} ${unit}s`;
|
|
147
|
+
}
|
|
148
|
+
function summaryLine(run) {
|
|
149
|
+
const noun = run.matches.length === 1 ? "signal" : "signals";
|
|
150
|
+
return `${run.matches.length} ${noun}. Warn-only. Nothing blocked.`;
|
|
151
|
+
}
|
package/dist/search-bench.js
CHANGED
|
@@ -39,7 +39,7 @@ export async function runSearchBench(configPath) {
|
|
|
39
39
|
const runs = await runCommitReplay(replay, profiles, replayDir);
|
|
40
40
|
replayRuns.push(...runs);
|
|
41
41
|
}
|
|
42
|
-
const leaderboard = summarize(profiles.map(({ profile }) => profile),
|
|
42
|
+
const leaderboard = summarize(profiles.map(({ profile }) => profile), allRuns);
|
|
43
43
|
const perCheck = summarizeByCheck(allRuns);
|
|
44
44
|
const summary = {
|
|
45
45
|
name: config.name,
|
|
@@ -246,6 +246,8 @@ function replayErrorRun(replayId, profileId, commit, error) {
|
|
|
246
246
|
async function runCli(cwd, args) {
|
|
247
247
|
const startedAt = Date.now();
|
|
248
248
|
const cliPath = process.argv[1];
|
|
249
|
+
if (!cliPath)
|
|
250
|
+
throw new Error("Could not resolve current CLI entrypoint.");
|
|
249
251
|
const { stdout } = await execFileAsync(process.execPath, [cliPath, ...args], {
|
|
250
252
|
cwd,
|
|
251
253
|
env: process.env,
|
|
@@ -341,7 +343,7 @@ function scoreSmokeRun(run) {
|
|
|
341
343
|
score -= (run.inputTokens / 1000) * 0.001;
|
|
342
344
|
return round(score);
|
|
343
345
|
}
|
|
344
|
-
function summarize(profiles,
|
|
346
|
+
function summarize(profiles, runs) {
|
|
345
347
|
const rows = profiles.map((profile) => {
|
|
346
348
|
const fixtureRuns = runs.filter((run) => run.profileId === profile.id && run.fixtureId);
|
|
347
349
|
const smokeRuns = runs.filter((run) => run.profileId === profile.id && run.smokeId);
|
package/dist/stupify.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { countPromptTokens, runSearch, searchRequest } from "./analysis.js";
|
|
5
5
|
import { searchChecks } from "./checks.js";
|
|
6
6
|
import { parseCommand } from "./command.js";
|
|
7
|
-
import {
|
|
7
|
+
import { counterScoutPlan } from "./counter-scout.js";
|
|
8
8
|
import { runDoctor } from "./doctor.js";
|
|
9
9
|
import { runHookCommand } from "./hooks.js";
|
|
10
10
|
import { firstRunModelBootstrap, loadLocalModel } from "./model.js";
|
|
@@ -67,7 +67,10 @@ export async function runSearchCommand(command, startedAt, ui = createCliUi({ qu
|
|
|
67
67
|
detail: (v) => `${v.summary.fileCount} files`,
|
|
68
68
|
});
|
|
69
69
|
try {
|
|
70
|
-
const
|
|
70
|
+
const scoutPlan = counterScoutPlan(changeSet, checks, maxCandidates);
|
|
71
|
+
if (!command.json)
|
|
72
|
+
ui.step(scoutPlanLine(scoutPlan, changeSet.summary.total));
|
|
73
|
+
const candidates = scoutPlan.targets;
|
|
71
74
|
const contexts = entityContextsFromChanges(candidates, changeSet.changes);
|
|
72
75
|
const targetsByPattern = countTargetsByPattern(contexts);
|
|
73
76
|
const targetsPreview = previewTargets(contexts);
|
|
@@ -108,6 +111,8 @@ export async function runSearchCommand(command, startedAt, ui = createCliUi({ qu
|
|
|
108
111
|
const searchContexts = profile?.context === "sem"
|
|
109
112
|
? contexts
|
|
110
113
|
: contexts.filter((context) => context.filePath && packedFiles.has(context.filePath));
|
|
114
|
+
if (!command.json)
|
|
115
|
+
ui.step(targetPlanLine(searchContexts, contexts.length, countTargetsByPattern(searchContexts)));
|
|
111
116
|
if (searchContexts.length === 0) {
|
|
112
117
|
return {
|
|
113
118
|
schemaVersion: "search.v1",
|
|
@@ -181,11 +186,14 @@ export async function runSearchCommand(command, startedAt, ui = createCliUi({ qu
|
|
|
181
186
|
};
|
|
182
187
|
}
|
|
183
188
|
if (batches.wasSplit && !command.json) {
|
|
184
|
-
ui.warn(`Search input is large; queued ${batches.batches.length} smaller
|
|
189
|
+
ui.warn(`Search input is large; queued ${batches.batches.length} smaller batches for ${searchContexts.length} targets (${maxSearchInputTokens} token cap).`);
|
|
185
190
|
if (batches.skippedTargets > 0) {
|
|
186
191
|
ui.warn(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
|
|
187
192
|
}
|
|
188
193
|
}
|
|
194
|
+
else if (!command.json) {
|
|
195
|
+
ui.step(`Search: ${searchContexts.length} targets in ${batches.batches.length} model batch (${maxSearchInputTokens} token cap)`);
|
|
196
|
+
}
|
|
189
197
|
const modelPath = await firstRunModelBootstrap(command.model, ui);
|
|
190
198
|
const model = await loadLocalModel(modelPath, command.model, "scout", ui);
|
|
191
199
|
const matches = [];
|
|
@@ -345,6 +353,28 @@ function formatStep(name, ms, count, detail) {
|
|
|
345
353
|
return `Model: ${count ?? 0} matches (${ms}ms)`;
|
|
346
354
|
return `${name}: ${ms}ms`;
|
|
347
355
|
}
|
|
356
|
+
function scoutPlanLine(plan, entitiesScanned) {
|
|
357
|
+
if (plan.targets.length === 0) {
|
|
358
|
+
return `Scout: deterministic counters scanned ${entitiesScanned} entities; no target/check pairs selected`;
|
|
359
|
+
}
|
|
360
|
+
return [
|
|
361
|
+
`Scout: deterministic counters scanned ${entitiesScanned} entities`,
|
|
362
|
+
`${plan.totalSignals} counter signals`,
|
|
363
|
+
`selected ${plan.targets.length}/${plan.totalSignals} target/check pairs (cap ${plan.maxTargets}, not exhaustive)`,
|
|
364
|
+
].join("; ");
|
|
365
|
+
}
|
|
366
|
+
function targetPlanLine(searchContexts, selectedTargets, targetsByPattern) {
|
|
367
|
+
const retained = searchContexts.length === selectedTargets
|
|
368
|
+
? `${searchContexts.length} selected targets`
|
|
369
|
+
: `${searchContexts.length}/${selectedTargets} selected targets retained after context packing`;
|
|
370
|
+
return `Targets: model will inspect ${retained}; ${formatCounts(targetsByPattern)}`;
|
|
371
|
+
}
|
|
372
|
+
function formatCounts(counts) {
|
|
373
|
+
const entries = Object.entries(counts).filter(([, count]) => count > 0);
|
|
374
|
+
if (entries.length === 0)
|
|
375
|
+
return "no target/check pairs";
|
|
376
|
+
return entries.map(([id, count]) => `${id}=${count}`).join(", ");
|
|
377
|
+
}
|
|
348
378
|
function sourceLabel(command) {
|
|
349
379
|
if (command.kind === "since")
|
|
350
380
|
return `since ${command.since}`;
|
package/package.json
CHANGED
package/src/analysis.ts
CHANGED
|
@@ -135,7 +135,7 @@ function stringSnapshot(value: unknown): string | undefined {
|
|
|
135
135
|
|
|
136
136
|
function limitSnapshot(value: string): string {
|
|
137
137
|
const lines = value.split(/\r?\n/);
|
|
138
|
-
const limit =
|
|
138
|
+
const limit = 12;
|
|
139
139
|
if (lines.length <= limit) return value;
|
|
140
140
|
return `${lines.slice(0, limit).join("\n")}
|
|
141
141
|
[stupify: snapshot shortened after ${limit} lines]`;
|
package/src/command.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_MODEL_ID, MODEL_REGISTRY } from "./constants.ts";
|
|
2
|
-
import type { Command, HookAction, ModelId
|
|
2
|
+
import type { Command, HookAction, ModelId } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
const DEFAULT_SINCE = "2 weeks ago";
|
|
5
5
|
const DEFAULT_MAX_CANDIDATES = 10;
|
|
@@ -13,7 +13,7 @@ type InputMode =
|
|
|
13
13
|
| Readonly<{ kind: "staged"; source: "staged" }>;
|
|
14
14
|
|
|
15
15
|
export function parseCommand(argv: readonly string[]): Command {
|
|
16
|
-
if (argv.length === 1 && isHelp(argv[0])) return { kind: "help" };
|
|
16
|
+
if (argv.length === 1 && argv[0] && isHelp(argv[0])) return { kind: "help" };
|
|
17
17
|
if (argv[0] === "bench") {
|
|
18
18
|
if (argv[1] !== "search" || !argv[2] || argv.length > 3) {
|
|
19
19
|
throw new Error("Usage: stupify bench search <config.json>");
|
package/src/constants.ts
CHANGED
package/src/counter-scout.ts
CHANGED
|
@@ -14,11 +14,26 @@ type SignalBucket = Readonly<{
|
|
|
14
14
|
|
|
15
15
|
const MAX_COUNTER_EXAMPLES_PER_CHECK = 4;
|
|
16
16
|
|
|
17
|
+
export type CounterScoutPlan = Readonly<{
|
|
18
|
+
buckets: readonly SignalBucket[];
|
|
19
|
+
totalSignals: number;
|
|
20
|
+
maxTargets: number;
|
|
21
|
+
targets: readonly SemCandidate[];
|
|
22
|
+
}>;
|
|
23
|
+
|
|
17
24
|
export function counterScoutTargets(
|
|
18
25
|
changeSet: SemChangeSet,
|
|
19
26
|
checks: readonly StupifyCheck[],
|
|
20
27
|
maxTargets: number,
|
|
21
28
|
): readonly SemCandidate[] {
|
|
29
|
+
return counterScoutPlan(changeSet, checks, maxTargets).targets;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function counterScoutPlan(
|
|
33
|
+
changeSet: SemChangeSet,
|
|
34
|
+
checks: readonly StupifyCheck[],
|
|
35
|
+
maxTargets: number,
|
|
36
|
+
): CounterScoutPlan {
|
|
22
37
|
const buckets = runSignalCounters(changeSet, checks);
|
|
23
38
|
const targets: SemCandidate[] = [];
|
|
24
39
|
let cursor = 0;
|
|
@@ -37,7 +52,12 @@ export function counterScoutTargets(
|
|
|
37
52
|
}
|
|
38
53
|
cursor += 1;
|
|
39
54
|
}
|
|
40
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
buckets,
|
|
57
|
+
totalSignals: buckets.reduce((sum, bucket) => sum + bucket.total, 0),
|
|
58
|
+
maxTargets,
|
|
59
|
+
targets,
|
|
60
|
+
};
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
export function runSignalCounters(
|
package/src/git.ts
CHANGED
|
@@ -40,6 +40,7 @@ export async function sourceRangeForRecentCommits(count: number): Promise<Source
|
|
|
40
40
|
|
|
41
41
|
const oldest = commits[0];
|
|
42
42
|
const newest = commits[commits.length - 1];
|
|
43
|
+
if (!oldest || !newest) throw new Error("Could not resolve recent commit range.");
|
|
43
44
|
const [base, target, shortBase, shortTarget] = await Promise.all([
|
|
44
45
|
revParse(`${oldest}^1`),
|
|
45
46
|
revParse(newest),
|
|
@@ -233,7 +234,7 @@ function statsFromDiff(diffText: string): NetDiffStats {
|
|
|
233
234
|
let deletions = 0;
|
|
234
235
|
for (const line of diffText.split(/\r?\n/)) {
|
|
235
236
|
const fileMatch = /^diff --git a\/.+ b\/(.+)$/.exec(line);
|
|
236
|
-
if (fileMatch) files.add(fileMatch[1]);
|
|
237
|
+
if (fileMatch?.[1]) files.add(fileMatch[1]);
|
|
237
238
|
else if (line.startsWith("+") && !line.startsWith("+++")) additions += 1;
|
|
238
239
|
else if (line.startsWith("-") && !line.startsWith("---")) deletions += 1;
|
|
239
240
|
}
|
package/src/model.ts
CHANGED
|
@@ -245,7 +245,7 @@ async function managedServerPid(runtime: ModelRuntime): Promise<number | null> {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function pidPath(
|
|
248
|
+
function pidPath(_runtime: ModelRuntime): string {
|
|
249
249
|
return path.join(cacheDir(), "llama-server.pid");
|
|
250
250
|
}
|
|
251
251
|
|
package/src/render.ts
CHANGED
|
@@ -30,9 +30,9 @@ ${format.success("No judgment-offload signals found.")}`;
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
return `${slopHeading()}
|
|
33
|
-
${run.matches.map((match, index) => `${index + 1}. ${format.label(match.patternId)}
|
|
34
33
|
${committerLabel(run)} (${sourceLabel(command)})
|
|
35
34
|
|
|
35
|
+
${run.matches.map((match, index) => `${index + 1}. ${format.label(match.patternId)}
|
|
36
36
|
${match.reason}
|
|
37
37
|
|
|
38
38
|
\`\`\`
|
|
@@ -41,7 +41,7 @@ ${match.snapshot ?? match.proof}
|
|
|
41
41
|
${format.muted(match.proof)}
|
|
42
42
|
|
|
43
43
|
${match.checkWhy ?? "This pattern may indicate judgment-offload."}`).join("\n\n")}
|
|
44
|
-
${format.muted(
|
|
44
|
+
${format.muted(summaryLine(run))}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function helpText(): string {
|
|
@@ -101,20 +101,31 @@ function sourceHint(command: SearchCommand): string {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function sourceLabel(command: SearchCommand): string {
|
|
104
|
-
if (command.kind === "staged") return "staged
|
|
105
|
-
if (command.kind === "since") return
|
|
104
|
+
if (command.kind === "staged") return "staged";
|
|
105
|
+
if (command.kind === "since") return sinceLabel(command.since);
|
|
106
106
|
if (command.kind === "commit") return `commit ${command.commit}`;
|
|
107
107
|
if (command.kind === "commits") return `last ${command.count} commits`;
|
|
108
|
-
return "stdin
|
|
108
|
+
return "stdin";
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function committerLabel(run: SearchRunJson): string {
|
|
112
|
-
const committers = (run.stats.committers ?? []).
|
|
112
|
+
const committers = humanCommitters(run.stats.committers ?? []).map(committerDisplayName);
|
|
113
113
|
if (committers.length === 0) return "unknown committer";
|
|
114
114
|
if (committers.length <= 3) return committers.join(", ");
|
|
115
115
|
return `${committers.slice(0, 3).join(", ")} +${committers.length - 3} more`;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function humanCommitters(committers: readonly string[]): readonly string[] {
|
|
119
|
+
const nonEmpty = committers.filter(Boolean);
|
|
120
|
+
const humans = nonEmpty.filter((committer) => !isBotCommitter(committer));
|
|
121
|
+
return humans.length > 0 ? humans : nonEmpty;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isBotCommitter(value: string): boolean {
|
|
125
|
+
return /(?:^|<)(?:github|dependabot|renovate)(?:\s|@|>)/i.test(value) ||
|
|
126
|
+
/(?:noreply@github\.com|bot@)/i.test(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
function committerDisplayName(value: string): string {
|
|
119
130
|
return value.replace(/\s*<[^>]+>\s*$/, "").trim() || value;
|
|
120
131
|
}
|
|
@@ -124,3 +135,20 @@ function slopHeading(): string {
|
|
|
124
135
|
return `${format.warn(format.heading(heading))}
|
|
125
136
|
${format.warn("=".repeat(heading.length))}`;
|
|
126
137
|
}
|
|
138
|
+
|
|
139
|
+
function sinceLabel(since: string): string {
|
|
140
|
+
const value = since.trim().toLowerCase();
|
|
141
|
+
if (value === "yesterday" || value === "1 day ago") return "yesterday";
|
|
142
|
+
const match = /^(\d+)\s+(day|week|month|year)s?\s+ago$/.exec(value);
|
|
143
|
+
if (!match) return `since ${since}`;
|
|
144
|
+
|
|
145
|
+
const count = Number(match[1]);
|
|
146
|
+
const unit = match[2];
|
|
147
|
+
if (count === 1) return `last ${unit}`;
|
|
148
|
+
return `last ${count} ${unit}s`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function summaryLine(run: SearchRunJson): string {
|
|
152
|
+
const noun = run.matches.length === 1 ? "signal" : "signals";
|
|
153
|
+
return `${run.matches.length} ${noun}. Warn-only. Nothing blocked.`;
|
|
154
|
+
}
|
package/src/search-bench.ts
CHANGED
|
@@ -99,7 +99,7 @@ export async function runSearchBench(configPath: string): Promise<string> {
|
|
|
99
99
|
replayRuns.push(...runs);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const leaderboard = summarize(profiles.map(({ profile }) => profile),
|
|
102
|
+
const leaderboard = summarize(profiles.map(({ profile }) => profile), allRuns);
|
|
103
103
|
const perCheck = summarizeByCheck(allRuns);
|
|
104
104
|
const summary: BenchSummary = {
|
|
105
105
|
name: config.name,
|
|
@@ -333,6 +333,7 @@ function replayErrorRun(
|
|
|
333
333
|
async function runCli(cwd: string, args: readonly string[]): Promise<SearchRunJson> {
|
|
334
334
|
const startedAt = Date.now();
|
|
335
335
|
const cliPath = process.argv[1];
|
|
336
|
+
if (!cliPath) throw new Error("Could not resolve current CLI entrypoint.");
|
|
336
337
|
const { stdout } = await execFileAsync(process.execPath, [cliPath, ...args], {
|
|
337
338
|
cwd,
|
|
338
339
|
env: process.env,
|
|
@@ -434,7 +435,6 @@ function scoreSmokeRun(run: SearchBenchRun): number {
|
|
|
434
435
|
|
|
435
436
|
function summarize(
|
|
436
437
|
profiles: readonly SearchProfile[],
|
|
437
|
-
fixtures: readonly SearchFixture[],
|
|
438
438
|
runs: readonly SearchBenchRun[],
|
|
439
439
|
): readonly ProfileResult[] {
|
|
440
440
|
const rows = profiles.map((profile) => {
|
package/src/stupify.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { countPromptTokens, runSearch, searchRequest, type SearchRequest } from "./analysis.ts";
|
|
6
6
|
import { searchChecks } from "./checks.ts";
|
|
7
7
|
import { parseCommand } from "./command.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { counterScoutPlan } from "./counter-scout.ts";
|
|
9
9
|
import { runDoctor } from "./doctor.ts";
|
|
10
10
|
import { runHookCommand } from "./hooks.ts";
|
|
11
11
|
import { firstRunModelBootstrap, loadLocalModel } from "./model.ts";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { semChangeSetForCommand } from "./sem-provider.ts";
|
|
22
22
|
import { createTracer } from "./trace.ts";
|
|
23
23
|
import { createCliUi, type CliUi } from "./ui.ts";
|
|
24
|
+
import type { CounterScoutPlan } from "./counter-scout.ts";
|
|
24
25
|
import type { SearchCommand, SearchMatch, SearchProfile, SearchRunJson, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
|
|
25
26
|
|
|
26
27
|
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
@@ -82,7 +83,9 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
82
83
|
);
|
|
83
84
|
|
|
84
85
|
try {
|
|
85
|
-
const
|
|
86
|
+
const scoutPlan = counterScoutPlan(changeSet, checks, maxCandidates);
|
|
87
|
+
if (!command.json) ui.step(scoutPlanLine(scoutPlan, changeSet.summary.total));
|
|
88
|
+
const candidates = scoutPlan.targets;
|
|
86
89
|
const contexts = entityContextsFromChanges(candidates, changeSet.changes);
|
|
87
90
|
const targetsByPattern = countTargetsByPattern(contexts);
|
|
88
91
|
const targetsPreview = previewTargets(contexts);
|
|
@@ -128,6 +131,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
128
131
|
const searchContexts = profile?.context === "sem"
|
|
129
132
|
? contexts
|
|
130
133
|
: contexts.filter((context) => context.filePath && packedFiles.has(context.filePath));
|
|
134
|
+
if (!command.json) ui.step(targetPlanLine(searchContexts, contexts.length, countTargetsByPattern(searchContexts)));
|
|
131
135
|
if (searchContexts.length === 0) {
|
|
132
136
|
return {
|
|
133
137
|
schemaVersion: "search.v1",
|
|
@@ -203,10 +207,12 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
if (batches.wasSplit && !command.json) {
|
|
206
|
-
ui.warn(`Search input is large; queued ${batches.batches.length} smaller
|
|
210
|
+
ui.warn(`Search input is large; queued ${batches.batches.length} smaller batches for ${searchContexts.length} targets (${maxSearchInputTokens} token cap).`);
|
|
207
211
|
if (batches.skippedTargets > 0) {
|
|
208
212
|
ui.warn(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
|
|
209
213
|
}
|
|
214
|
+
} else if (!command.json) {
|
|
215
|
+
ui.step(`Search: ${searchContexts.length} targets in ${batches.batches.length} model batch (${maxSearchInputTokens} token cap)`);
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
const modelPath = await firstRunModelBootstrap(command.model, ui);
|
|
@@ -442,6 +448,35 @@ function formatStep(name: string, ms: number, count?: number, detail?: string):
|
|
|
442
448
|
return `${name}: ${ms}ms`;
|
|
443
449
|
}
|
|
444
450
|
|
|
451
|
+
function scoutPlanLine(plan: CounterScoutPlan, entitiesScanned: number): string {
|
|
452
|
+
if (plan.targets.length === 0) {
|
|
453
|
+
return `Scout: deterministic counters scanned ${entitiesScanned} entities; no target/check pairs selected`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return [
|
|
457
|
+
`Scout: deterministic counters scanned ${entitiesScanned} entities`,
|
|
458
|
+
`${plan.totalSignals} counter signals`,
|
|
459
|
+
`selected ${plan.targets.length}/${plan.totalSignals} target/check pairs (cap ${plan.maxTargets}, not exhaustive)`,
|
|
460
|
+
].join("; ");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function targetPlanLine(
|
|
464
|
+
searchContexts: readonly SemContext[],
|
|
465
|
+
selectedTargets: number,
|
|
466
|
+
targetsByPattern: Record<string, number>,
|
|
467
|
+
): string {
|
|
468
|
+
const retained = searchContexts.length === selectedTargets
|
|
469
|
+
? `${searchContexts.length} selected targets`
|
|
470
|
+
: `${searchContexts.length}/${selectedTargets} selected targets retained after context packing`;
|
|
471
|
+
return `Targets: model will inspect ${retained}; ${formatCounts(targetsByPattern)}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function formatCounts(counts: Record<string, number>): string {
|
|
475
|
+
const entries = Object.entries(counts).filter(([, count]) => count > 0);
|
|
476
|
+
if (entries.length === 0) return "no target/check pairs";
|
|
477
|
+
return entries.map(([id, count]) => `${id}=${count}`).join(", ");
|
|
478
|
+
}
|
|
479
|
+
|
|
445
480
|
function sourceLabel(command: SearchCommand): string {
|
|
446
481
|
if (command.kind === "since") return `since ${command.since}`;
|
|
447
482
|
if (command.kind === "commit") return `commit ${command.commit}`;
|