@stupify/cli 0.0.12 → 0.0.14
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/command.js +2 -2
- 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 +10 -2
- package/dist/git.js +3 -1
- package/dist/model.js +1 -1
- package/dist/render.js +1 -1
- package/dist/search-bench.js +4 -2
- package/dist/stupify.js +33 -3
- package/package.json +1 -1
- package/src/command.ts +3 -3
- package/src/constants.ts +1 -1
- package/src/counter-scout.ts +22 -2
- package/src/git.ts +2 -1
- package/src/model.ts +1 -1
- package/src/render.ts +1 -1
- package/src/search-bench.ts +2 -2
- package/src/stupify.ts +38 -3
package/dist/command.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DEFAULT_MODEL_ID, MODEL_REGISTRY } from "./constants.js";
|
|
2
2
|
const DEFAULT_SINCE = "2 weeks ago";
|
|
3
|
-
const DEFAULT_MAX_CANDIDATES =
|
|
3
|
+
const DEFAULT_MAX_CANDIDATES = 50;
|
|
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
|
-
const MAX_COUNTER_EXAMPLES_PER_CHECK =
|
|
1
|
+
const MAX_COUNTER_EXAMPLES_PER_CHECK = 20;
|
|
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
|
@@ -61,7 +61,7 @@ Options:
|
|
|
61
61
|
--commits <count> Search the net diff across the last N non-merge commits.
|
|
62
62
|
--stdin Read a git diff from stdin.
|
|
63
63
|
--debug-sem Print sem commands and stderr.
|
|
64
|
-
--max-candidates <n> Max semantic search targets. Default:
|
|
64
|
+
--max-candidates <n> Max semantic search targets. Default: 50.
|
|
65
65
|
--max-search-input-tokens <n>
|
|
66
66
|
Max search input tokens before skipping. Default: 12000.
|
|
67
67
|
--checks <ids> Comma-separated pattern ids.
|
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/command.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
const DEFAULT_MAX_CANDIDATES =
|
|
5
|
+
const DEFAULT_MAX_CANDIDATES = 50;
|
|
6
6
|
const DEFAULT_MAX_SEARCH_INPUT_TOKENS = 12_000;
|
|
7
7
|
|
|
8
8
|
type InputMode =
|
|
@@ -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
|
@@ -12,13 +12,28 @@ type SignalBucket = Readonly<{
|
|
|
12
12
|
examples: readonly Signal[];
|
|
13
13
|
}>;
|
|
14
14
|
|
|
15
|
-
const MAX_COUNTER_EXAMPLES_PER_CHECK =
|
|
15
|
+
const MAX_COUNTER_EXAMPLES_PER_CHECK = 20;
|
|
16
|
+
|
|
17
|
+
export type CounterScoutPlan = Readonly<{
|
|
18
|
+
buckets: readonly SignalBucket[];
|
|
19
|
+
totalSignals: number;
|
|
20
|
+
maxTargets: number;
|
|
21
|
+
targets: readonly SemCandidate[];
|
|
22
|
+
}>;
|
|
16
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
|
@@ -67,7 +67,7 @@ Options:
|
|
|
67
67
|
--commits <count> Search the net diff across the last N non-merge commits.
|
|
68
68
|
--stdin Read a git diff from stdin.
|
|
69
69
|
--debug-sem Print sem commands and stderr.
|
|
70
|
-
--max-candidates <n> Max semantic search targets. Default:
|
|
70
|
+
--max-candidates <n> Max semantic search targets. Default: 50.
|
|
71
71
|
--max-search-input-tokens <n>
|
|
72
72
|
Max search input tokens before skipping. Default: 12000.
|
|
73
73
|
--checks <ids> Comma-separated pattern ids.
|
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}`;
|