@stupify/cli 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +26 -31
  2. package/dist/analysis.d.ts +11 -9
  3. package/dist/analysis.js +30 -173
  4. package/dist/checks.d.ts +1 -0
  5. package/dist/checks.js +89 -2
  6. package/dist/command.js +55 -91
  7. package/dist/constants.d.ts +1 -1
  8. package/dist/constants.js +1 -1
  9. package/dist/counter-scout.js +70 -8
  10. package/dist/doctor.d.ts +4 -0
  11. package/dist/doctor.js +131 -0
  12. package/dist/git.d.ts +4 -1
  13. package/dist/git.js +34 -0
  14. package/dist/hooks.d.ts +3 -0
  15. package/dist/hooks.js +117 -0
  16. package/dist/model.d.ts +1 -15
  17. package/dist/model.js +37 -21
  18. package/dist/prompts.d.ts +8 -5
  19. package/dist/prompts.js +58 -168
  20. package/dist/render.d.ts +2 -2
  21. package/dist/render.js +70 -78
  22. package/dist/repomix-provider.d.ts +10 -2
  23. package/dist/repomix-provider.js +62 -11
  24. package/dist/search-bench.d.ts +1 -0
  25. package/dist/search-bench.js +675 -0
  26. package/dist/search-profile.d.ts +6 -0
  27. package/dist/search-profile.js +73 -0
  28. package/dist/sem-provider.d.ts +2 -2
  29. package/dist/sem-provider.js +33 -7
  30. package/dist/stupify.d.ts +2 -0
  31. package/dist/stupify.js +183 -333
  32. package/dist/types.d.ts +193 -109
  33. package/package.json +1 -1
  34. package/src/analysis.ts +48 -268
  35. package/src/checks.ts +91 -2
  36. package/src/command.ts +62 -107
  37. package/src/constants.ts +1 -1
  38. package/src/counter-scout.ts +63 -7
  39. package/src/doctor.ts +140 -0
  40. package/src/git.ts +35 -1
  41. package/src/hooks.ts +134 -0
  42. package/src/model.ts +39 -26
  43. package/src/prompts.ts +66 -202
  44. package/src/render.ts +68 -79
  45. package/src/repomix-provider.ts +66 -10
  46. package/src/search-bench.ts +783 -0
  47. package/src/search-profile.ts +89 -0
  48. package/src/sem-provider.ts +36 -9
  49. package/src/stupify.ts +213 -526
  50. package/src/types.ts +195 -119
  51. package/dist/batcher.d.ts +0 -3
  52. package/dist/batcher.js +0 -142
  53. package/dist/candidate-context.d.ts +0 -2
  54. package/dist/candidate-context.js +0 -40
  55. package/dist/experiment.d.ts +0 -1
  56. package/dist/experiment.js +0 -225
  57. package/src/batcher.ts +0 -198
  58. package/src/candidate-context.ts +0 -43
  59. package/src/experiment.ts +0 -317
package/src/render.ts CHANGED
@@ -1,44 +1,42 @@
1
1
  import { VERSION } from "./constants.ts";
2
- import type { AnalysisReport, AnalyzeCommand } from "./types.ts";
2
+ import type { SearchCommand, SearchRunJson } from "./types.ts";
3
3
 
4
- export function renderReport(report: AnalysisReport, command: AnalyzeCommand): string {
5
- if (command.json) {
6
- return JSON.stringify({
7
- schemaVersion: "0.4",
8
- model: { id: report.run.modelId },
9
- checks: report.run.checkIds,
10
- run: report.run,
11
- findings: report.result.findings,
12
- summary: report.result.summary,
13
- }, null, 2);
4
+ export function renderSearchRun(run: SearchRunJson, command: SearchCommand): string {
5
+ if (command.json) return JSON.stringify(run, null, 2);
6
+
7
+ if (run.stats.skipped && run.stats.skipReason === "input_too_large") {
8
+ return `🧙 stupify 🪄
9
+ Search input is too large for precise local search.
10
+ Size:
11
+ ~${run.stats.inputTokens ?? "unknown"} tokens
12
+ Limit:
13
+ ${run.stats.inputTokenCap ?? "unknown"} tokens
14
+ Stupify skipped the search rather than review truncated context.
15
+ Nothing was blocked.
16
+ Try:
17
+ stupify ${sourceHint(command)} --max-search-input-tokens ${Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2)}`;
18
+ }
19
+
20
+ if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
21
+ return `🧙 stupify 🪄
22
+ Search complete.
23
+ Patterns: ${run.patterns.join(", ")}
24
+ No search targets found.`;
14
25
  }
15
26
 
16
- if (report.run.engine === "sem") {
17
- return `Search:
18
- ${report.run.entitiesScanned} entities scanned
19
- ${report.run.candidateCount} candidate entities found
20
- Audit:
21
- ${report.run.auditedCandidateCount} candidates inspected
22
- ${report.result.findings.length} findings
23
- ${renderAuditStats(report)}
24
- ${renderWarnings(report)}
25
- Findings:
26
- ${renderFindings(report)}
27
- Timing:
28
- total_ms=${report.run.timingsMs.total} entity_diff_ms=${report.run.timingsMs.diff} model_ms=${report.run.timingsMs.modelLoad} scout_ms=${report.run.timingsMs.search} context_audit_ms=${report.run.timingsMs.audit}`;
27
+ if (run.matches.length === 0) {
28
+ return `🧙 stupify 🪄
29
+ Search complete.
30
+ Patterns: ${run.patterns.join(", ")}
31
+ No judgment-offload signals found.`;
29
32
  }
30
33
 
31
- return `Search:
32
- ${report.run.batchesScanned} batches scanned
33
- ${report.run.candidateCount} candidate regions found
34
- Audit:
35
- ${report.run.auditedCandidateCount} candidates inspected
36
- ${report.result.findings.length} findings
37
- ${renderWarnings(report)}
38
- Findings:
39
- ${renderFindings(report)}
40
- Timing:
41
- total_ms=${report.run.timingsMs.total} diff_ms=${report.run.timingsMs.diff} model_ms=${report.run.timingsMs.modelLoad} search_ms=${report.run.timingsMs.search} audit_ms=${report.run.timingsMs.audit}`;
34
+ return `🧙 stupify 🪄
35
+ Possible judgment-offload detected:
36
+ ${run.matches.map((match, index) => `${index + 1}. ${match.patternId}
37
+ ${match.reason}
38
+ Proof: ${match.proof}`).join("\n")}
39
+ Search mode is warn-only.`;
42
40
  }
43
41
 
44
42
  export function helpText(): string {
@@ -49,59 +47,50 @@ Usage:
49
47
  stupify --since "2 weeks ago"
50
48
  stupify --commit <commit>
51
49
  stupify --commits <count>
52
- stupify experiment <config.json>
50
+ stupify --staged
51
+ stupify --mode search --staged
52
+ stupify hook install|uninstall|status
53
+ stupify doctor
54
+ stupify bench search experiments/search-bench.json
53
55
  git diff HEAD~1..HEAD | stupify --stdin
54
56
 
55
57
  Options:
56
- --since <date> Analyze the net diff from the first commit before this git date to HEAD.
57
- --commit <commit> Analyze one commit as a net diff.
58
- --commits <count> Analyze the net diff across the last N non-merge commits.
59
- --stdin Read a git diff from stdin.
60
- --engine <engine> raw-diff or sem. Default: raw-diff.
61
- --scout <mode> llm or counter for --engine sem. Default: counter.
62
- --audit-context <mode>
63
- none or repomix for --engine sem. Default: repomix.
64
- --audit-prompt <name> strict or high_bar for --engine sem. Default: high_bar.
65
- --debug-sem Print sem commands and stderr.
66
- --debug-targets Include audited sem target details in JSON output.
67
- --max-candidates <n> Max semantic candidates for --engine sem. Default: 10.
68
- --audit-batch-size <n>
69
- Semantic candidates per audit model call. Default: 25.
70
- --max-audit-input-tokens <n>
71
- Max findings-audit input tokens before splitting. Default: 20000.
72
- --audit-concurrency <n>
73
- Parallel findings-audit model calls. Default: 2.
74
- --checks <ids> Comma-separated check ids.
75
- --model <id> gemma-4-e2b, gemma-4-e4b, gemma-4-26b-a4b, qwen3-4b-magicquant, qwen2.5-coder-1.5b, qwen2.5-coder-7b, or qwen2.5-coder-32b.
76
- --json Print JSON only.
58
+ --staged Search staged changes.
59
+ --mode <mode> search. Search is the only analysis mode.
60
+ --since <date> Search the net diff from the first commit before this git date to HEAD.
61
+ --commit <commit> Search one commit as a net diff.
62
+ --commits <count> Search the net diff across the last N non-merge commits.
63
+ --stdin Read a git diff from stdin.
64
+ --debug-sem Print sem commands and stderr.
65
+ --max-candidates <n> Max semantic search targets. Default: 10.
66
+ --max-search-input-tokens <n>
67
+ Max search input tokens before skipping. Default: 12000.
68
+ --checks <ids> Comma-separated pattern ids.
69
+ --model <id> gemma-4-e2b, gemma-4-e4b, gemma-4-26b-a4b, qwen3-4b-magicquant, qwen2.5-coder-1.5b, qwen2.5-coder-7b, or qwen2.5-coder-32b.
70
+ --search-profile <path>
71
+ Dev/bench-only search profile override.
72
+ --include-counter-reason-in-prompt
73
+ Debug/bench-only: include counter reason in the model prompt.
74
+ --json Print JSON only.
75
+
76
+ Diagnostics:
77
+ stupify doctor Check local setup, hook status, and privacy boundary.
77
78
 
78
79
  Default:
79
80
  stupify is equivalent to stupify --since "2 weeks ago".
80
81
 
81
- Not included:
82
- Baselines, sharing, hosted server calls, Ollama, GitHub, dashboards, or repo-wide scanning.
83
- `;
84
- }
85
-
86
- function renderFindings(report: AnalysisReport): string {
87
- if (report.result.findings.length === 0) return " None.";
82
+ Pipeline:
83
+ sem diff -> counter scout -> Repomix context -> local search model.
88
84
 
89
- return report.result.findings
90
- .map((finding) => `- ${finding.checkId}
91
- ${finding.why}
92
- Proof: ${finding.proof}`)
93
- .join("\n");
94
- }
95
-
96
- function renderWarnings(report: AnalysisReport): string {
97
- if (report.run.warnings.length === 0) return "";
98
- return `Warnings:
99
- ${report.run.warnings.map((warning) => ` ${warning}`).join("\n")}
85
+ Not included:
86
+ Findings audit, validators, judges, baselines, sharing, hosted server calls, GitHub, dashboards, or repo-wide crawling.
100
87
  `;
101
88
  }
102
89
 
103
- function renderAuditStats(report: AnalysisReport): string {
104
- const stats = report.run.auditStats;
105
- if (!stats) return "";
106
- return ` ${stats.totalTargets} targets reviewed: ${stats.finding} finding, ${stats.uncertain} uncertain, ${stats.clean} clean, ${stats.invalid} invalid`;
90
+ function sourceHint(command: SearchCommand): string {
91
+ if (command.kind === "staged") return "--staged";
92
+ if (command.kind === "since") return `--since "${command.since}"`;
93
+ if (command.kind === "commit") return `--commit ${command.commit}`;
94
+ if (command.kind === "commits") return `--commits ${command.count}`;
95
+ return "--stdin";
107
96
  }
@@ -2,18 +2,20 @@ import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { pack, setLogLevel } from "repomix";
5
- import type { SemCandidate, SemChange, SemContext, SemContextPack } from "./types.ts";
5
+ import type { RepomixSearchConfig, SemCandidate, SemChange, SemContext, SemContextPack } from "./types.ts";
6
6
 
7
7
  const MAX_PACK_FILE_SIZE_BYTES = 48 * 1024;
8
8
  const MAX_PACK_TOTAL_SIZE_BYTES = 128 * 1024;
9
9
 
10
10
  export function emptyContextPack(): SemContextPack {
11
+ const config = repomixSearchConfig();
11
12
  return {
12
13
  provider: "repomix",
13
14
  filePaths: [],
14
15
  totalCharacters: 0,
15
16
  totalTokens: 0,
16
17
  text: "",
18
+ config,
17
19
  };
18
20
  }
19
21
 
@@ -21,10 +23,14 @@ export async function repomixContextPack(
21
23
  cwd: string,
22
24
  contexts: readonly SemContext[],
23
25
  changes: readonly SemChange[],
26
+ config = repomixSearchConfig(),
24
27
  ): Promise<SemContextPack> {
25
- const filePaths = await candidateFilePaths(cwd, contexts, changes);
28
+ const filePaths = await candidateFilePaths(cwd, contexts, changes, config);
26
29
  if (filePaths.length === 0) {
27
- return emptyContextPack();
30
+ return {
31
+ ...emptyContextPack(),
32
+ config,
33
+ };
28
34
  }
29
35
 
30
36
  setLogLevel(-1);
@@ -35,7 +41,7 @@ export async function repomixContextPack(
35
41
  [cwd],
36
42
  {
37
43
  cwd,
38
- input: { maxFileSize: MAX_PACK_FILE_SIZE_BYTES },
44
+ input: { maxFileSize: config.maxFileSizeBytes },
39
45
  output: {
40
46
  filePath: outputPath,
41
47
  style: "xml",
@@ -44,10 +50,10 @@ export async function repomixContextPack(
44
50
  directoryStructure: false,
45
51
  files: true,
46
52
  removeComments: false,
47
- removeEmptyLines: true,
48
- compress: true,
53
+ removeEmptyLines: config.removeEmptyLines,
54
+ compress: config.compress,
49
55
  topFilesLength: 0,
50
- showLineNumbers: true,
56
+ showLineNumbers: config.showLineNumbers,
51
57
  truncateBase64: true,
52
58
  copyToClipboard: false,
53
59
  includeFullDirectoryStructure: false,
@@ -65,7 +71,7 @@ export async function repomixContextPack(
65
71
  useGitignore: true,
66
72
  useDotIgnore: true,
67
73
  useDefaultPatterns: true,
68
- customPatterns: [],
74
+ customPatterns: [...config.ignorePatterns],
69
75
  },
70
76
  security: { enableSecurityCheck: false },
71
77
  tokenCount: { encoding: "o200k_base" },
@@ -80,6 +86,7 @@ export async function repomixContextPack(
80
86
  totalCharacters: result.totalCharacters,
81
87
  totalTokens: result.totalTokens,
82
88
  text: await readFile(outputPath, "utf8"),
89
+ config,
83
90
  };
84
91
  } finally {
85
92
  await rm(tempDir, { recursive: true, force: true });
@@ -120,6 +127,7 @@ async function candidateFilePaths(
120
127
  cwd: string,
121
128
  contexts: readonly SemContext[],
122
129
  changes: readonly SemChange[],
130
+ config: RepomixSearchConfig,
123
131
  ): Promise<readonly string[]> {
124
132
  const byEntityId = new Map(changes.map((change) => [change.entityId, change.filePath]));
125
133
  const paths = contexts.flatMap((context) => context.filePath ?? byEntityId.get(context.entityId) ?? []);
@@ -127,15 +135,63 @@ async function candidateFilePaths(
127
135
  const selected = [];
128
136
  let totalBytes = 0;
129
137
  for (const filePath of safePaths) {
138
+ if (matchesAnyPattern(filePath, config.ignorePatterns)) continue;
130
139
  const bytes = await fileSize(cwd, filePath);
131
- if (bytes === null || bytes > MAX_PACK_FILE_SIZE_BYTES) continue;
132
- if (totalBytes + bytes > MAX_PACK_TOTAL_SIZE_BYTES) continue;
140
+ if (bytes === null || bytes > config.maxFileSizeBytes) continue;
141
+ if (totalBytes + bytes > config.maxTotalSizeBytes) continue;
133
142
  totalBytes += bytes;
134
143
  selected.push(filePath);
135
144
  }
136
145
  return selected;
137
146
  }
138
147
 
148
+ export function repomixSearchConfig(): RepomixSearchConfig {
149
+ return {
150
+ compress: envBoolean("STUPIFY_REPOMIX_COMPRESS", true),
151
+ showLineNumbers: envBoolean("STUPIFY_REPOMIX_SHOW_LINE_NUMBERS", true),
152
+ removeEmptyLines: envBoolean("STUPIFY_REPOMIX_REMOVE_EMPTY_LINES", true),
153
+ maxFileSizeBytes: envInteger("STUPIFY_REPOMIX_MAX_FILE_BYTES", MAX_PACK_FILE_SIZE_BYTES),
154
+ maxTotalSizeBytes: envInteger("STUPIFY_REPOMIX_MAX_TOTAL_BYTES", MAX_PACK_TOTAL_SIZE_BYTES),
155
+ ignorePatterns: envList("STUPIFY_REPOMIX_IGNORE_PATTERNS"),
156
+ };
157
+ }
158
+
159
+ function envBoolean(name: string, fallback: boolean): boolean {
160
+ const value = process.env[name];
161
+ if (value === undefined || value === "") return fallback;
162
+ return /^(1|true|yes|on)$/i.test(value);
163
+ }
164
+
165
+ function envInteger(name: string, fallback: number): number {
166
+ const value = Number(process.env[name]);
167
+ return Number.isInteger(value) && value > 0 ? value : fallback;
168
+ }
169
+
170
+ function envList(name: string): readonly string[] {
171
+ return (process.env[name] ?? "")
172
+ .split(",")
173
+ .map((item) => item.trim())
174
+ .filter(Boolean);
175
+ }
176
+
177
+ function matchesAnyPattern(filePath: string, patterns: readonly string[]): boolean {
178
+ return patterns.some((pattern) => matchesPattern(filePath, pattern));
179
+ }
180
+
181
+ function matchesPattern(filePath: string, pattern: string): boolean {
182
+ if (pattern === filePath) return true;
183
+ if (!pattern.includes("*")) return false;
184
+ const escaped = pattern
185
+ .split("*")
186
+ .map(escapeRegExp)
187
+ .join(".*");
188
+ return new RegExp(`^${escaped}$`).test(filePath);
189
+ }
190
+
191
+ function escapeRegExp(value: string): string {
192
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
193
+ }
194
+
139
195
  function isSafeRelativeFilePath(value: string): boolean {
140
196
  if (!value || path.isAbsolute(value)) return false;
141
197
  const normalized = path.normalize(value);