@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 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 = 10;
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) {
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.0.12";
1
+ export declare const VERSION = "0.0.14";
2
2
  import type { ModelConfig, ModelId } from "./types.ts";
3
3
  export declare const DEFAULT_MODEL_ID: ModelId;
4
4
  export declare const MODEL_REGISTRY: Record<ModelId, ModelConfig>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "0.0.12";
1
+ export const VERSION = "0.0.14";
2
2
  export const DEFAULT_MODEL_ID = "gemma-4-e2b";
3
3
  export const MODEL_REGISTRY = {
4
4
  "gemma-4-e2b": {
@@ -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 {};
@@ -1,5 +1,8 @@
1
- const MAX_COUNTER_EXAMPLES_PER_CHECK = 4;
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 targets;
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
@@ -190,7 +190,7 @@ async function managedServerPid(runtime) {
190
190
  return null;
191
191
  }
192
192
  }
193
- function pidPath(runtime) {
193
+ function pidPath(_runtime) {
194
194
  return path.join(cacheDir(), "llama-server.pid");
195
195
  }
196
196
  function envInteger(name, fallback) {
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: 10.
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.
@@ -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), fixtures.map(({ fixture }) => fixture), allRuns);
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, fixtures, runs) {
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 { counterScoutTargets } from "./counter-scout.js";
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 candidates = counterScoutTargets(changeSet, checks, maxCandidates);
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 search batches.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stupify/cli",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Local-only diagnostic CLI for checking whether AI is making you dumber.",
5
5
  "private": false,
6
6
  "type": "module",
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, SearchSource } from "./types.ts";
2
+ import type { Command, HookAction, ModelId } from "./types.ts";
3
3
 
4
4
  const DEFAULT_SINCE = "2 weeks ago";
5
- const DEFAULT_MAX_CANDIDATES = 10;
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
@@ -1,4 +1,4 @@
1
- export const VERSION = "0.0.12";
1
+ export const VERSION = "0.0.14";
2
2
  import type { ModelConfig, ModelId } from "./types.ts";
3
3
 
4
4
  export const DEFAULT_MODEL_ID: ModelId = "gemma-4-e2b";
@@ -12,13 +12,28 @@ type SignalBucket = Readonly<{
12
12
  examples: readonly Signal[];
13
13
  }>;
14
14
 
15
- const MAX_COUNTER_EXAMPLES_PER_CHECK = 4;
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 targets;
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(runtime: ModelRuntime): string {
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: 10.
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.
@@ -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), fixtures.map(({ fixture }) => fixture), allRuns);
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 { counterScoutTargets } from "./counter-scout.ts";
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 candidates = counterScoutTargets(changeSet, checks, maxCandidates);
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 search batches.`);
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}`;