@stupify/cli 0.0.16 → 0.2.0
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/.review/CORPUS.md +44 -0
- package/.review/CORPUS.template.md +73 -0
- package/.review/REVIEW-PROMPT.md +52 -0
- package/.review/RUBRIC.md +46 -0
- package/LICENSE +1 -1
- package/README.md +95 -37
- package/package.json +27 -26
- package/packs/antirez.md +10 -0
- package/packs/anton-kropp.md +10 -0
- package/packs/dhh.md +10 -0
- package/packs/dtolnay.md +10 -0
- package/packs/jarred-sumner.md +9 -0
- package/packs/mitchell-hashimoto.md +10 -0
- package/packs/rich-harris.md +10 -0
- package/packs/simon-willison.md +10 -0
- package/packs/sindre-sorhus.md +10 -0
- package/packs/tanner-linsley.md +10 -0
- package/packs/zod.md +10 -0
- package/src/cli.ts +626 -0
- package/src/prime-install.test.ts +109 -0
- package/src/prime.ts +50 -0
- package/src/review-sweep.test.ts +101 -0
- package/src/review-sweep.ts +526 -0
- package/dist/analysis.d.ts +0 -16
- package/dist/analysis.js +0 -168
- package/dist/cache.d.ts +0 -2
- package/dist/cache.js +0 -57
- package/dist/checks.d.ts +0 -4
- package/dist/checks.js +0 -228
- package/dist/command.d.ts +0 -2
- package/dist/command.js +0 -147
- package/dist/constants.d.ts +0 -4
- package/dist/constants.js +0 -53
- package/dist/counter-scout.d.ts +0 -21
- package/dist/counter-scout.js +0 -167
- package/dist/diff.d.ts +0 -1
- package/dist/diff.js +0 -10
- package/dist/doctor.d.ts +0 -16
- package/dist/doctor.js +0 -143
- package/dist/git.d.ts +0 -17
- package/dist/git.js +0 -368
- package/dist/hooks.d.ts +0 -5
- package/dist/hooks.js +0 -135
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/model.d.ts +0 -11
- package/dist/model.js +0 -296
- package/dist/prompts.d.ts +0 -8
- package/dist/prompts.js +0 -89
- package/dist/render.d.ts +0 -6
- package/dist/render.js +0 -295
- package/dist/repomix-provider.d.ts +0 -12
- package/dist/repomix-provider.js +0 -196
- package/dist/search-bench.d.ts +0 -1
- package/dist/search-bench.js +0 -677
- package/dist/search-profile.d.ts +0 -6
- package/dist/search-profile.js +0 -73
- package/dist/sem-provider.d.ts +0 -2
- package/dist/sem-provider.js +0 -255
- package/dist/stupify.d.ts +0 -38
- package/dist/stupify.js +0 -505
- package/dist/trace.d.ts +0 -31
- package/dist/trace.js +0 -86
- package/dist/types.d.ts +0 -341
- package/dist/types.js +0 -6
- package/dist/ui.d.ts +0 -34
- package/dist/ui.js +0 -143
- package/src/analysis.ts +0 -223
- package/src/cache.ts +0 -63
- package/src/checks.ts +0 -231
- package/src/command.ts +0 -173
- package/src/constants.ts +0 -56
- package/src/counter-scout.ts +0 -195
- package/src/diff.ts +0 -9
- package/src/doctor.ts +0 -166
- package/src/git.ts +0 -380
- package/src/hooks.ts +0 -151
- package/src/index.ts +0 -1
- package/src/model.ts +0 -367
- package/src/prompts.ts +0 -100
- package/src/render.ts +0 -328
- package/src/repomix-provider.ts +0 -219
- package/src/search-bench.ts +0 -783
- package/src/search-profile.ts +0 -89
- package/src/sem-provider.ts +0 -300
- package/src/stupify.ts +0 -604
- package/src/trace.ts +0 -126
- package/src/types.ts +0 -362
- package/src/ui.ts +0 -187
package/dist/model.js
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, open, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { MODEL_REGISTRY } from "./constants.js";
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
const LLAMA_SERVER_HOST = "127.0.0.1";
|
|
9
|
-
export async function firstRunModelBootstrap(modelId, ui) {
|
|
10
|
-
const selectedModel = MODEL_REGISTRY[modelId];
|
|
11
|
-
const modelDir = path.join(cacheDir(), "models");
|
|
12
|
-
const modelPath = path.join(modelDir, selectedModel.file);
|
|
13
|
-
if (await exists(modelPath))
|
|
14
|
-
return modelPath;
|
|
15
|
-
ui.note(`No local Stupify model found.
|
|
16
|
-
Stupify runs locally.
|
|
17
|
-
Download this model now?
|
|
18
|
-
Model: ${selectedModel.name}
|
|
19
|
-
Size: ${selectedModel.size}`, "Setup", { force: true });
|
|
20
|
-
if (!(await ui.confirm("Continue?")))
|
|
21
|
-
throw new Error("Setup cancelled.");
|
|
22
|
-
await mkdir(modelDir, { recursive: true });
|
|
23
|
-
await downloadModel(modelPath, selectedModel.url, ui);
|
|
24
|
-
if (!(await exists(modelPath)))
|
|
25
|
-
throw new Error("Model download failed: file was not created.");
|
|
26
|
-
return modelPath;
|
|
27
|
-
}
|
|
28
|
-
export async function loadLocalModel(modelPath, modelId, profile, ui) {
|
|
29
|
-
const selectedModel = MODEL_REGISTRY[modelId];
|
|
30
|
-
const runtime = modelRuntime(profile);
|
|
31
|
-
const runningModel = await runningServerModel(runtime.baseUrl);
|
|
32
|
-
if (runningModel) {
|
|
33
|
-
if (runningModel !== modelId)
|
|
34
|
-
await stopManagedServer(runtime, ui);
|
|
35
|
-
if (runningModel === modelId) {
|
|
36
|
-
ui.info(`Using local model: ${selectedModel.name}`);
|
|
37
|
-
return {
|
|
38
|
-
id: modelId,
|
|
39
|
-
name: selectedModel.name,
|
|
40
|
-
baseUrl: runtime.baseUrl,
|
|
41
|
-
profile,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
await ensureLlamaServerBinary();
|
|
46
|
-
await startLlamaServer(modelPath, modelId, selectedModel.name, runtime, ui);
|
|
47
|
-
const ready = ui.spinner(`Waiting for local ${profile} model`);
|
|
48
|
-
try {
|
|
49
|
-
await waitForServer(runtime.baseUrl, modelId);
|
|
50
|
-
ready.stop(`Local ${profile} model ready`);
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
ready.error(`Local ${profile} model failed to start`);
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
id: modelId,
|
|
58
|
-
name: selectedModel.name,
|
|
59
|
-
baseUrl: runtime.baseUrl,
|
|
60
|
-
profile,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function modelRuntime(profile) {
|
|
64
|
-
const baseUrl = process.env.STUPIFY_SCOUT_LLAMA_SERVER_URL ??
|
|
65
|
-
process.env.STUPIFY_LLAMA_SERVER_URL ??
|
|
66
|
-
"http://127.0.0.1:8091";
|
|
67
|
-
return {
|
|
68
|
-
profile,
|
|
69
|
-
baseUrl,
|
|
70
|
-
port: new URL(baseUrl).port || "8091",
|
|
71
|
-
contextSize: envInteger("STUPIFY_LLAMA_CONTEXT") ?? 65_536,
|
|
72
|
-
reasoning: "off",
|
|
73
|
-
gpuLayers: envInteger("STUPIFY_LLAMA_GPU_LAYERS") ?? 999,
|
|
74
|
-
batchSize: envInteger("STUPIFY_LLAMA_BATCH") ?? 2_048,
|
|
75
|
-
ubatchSize: envInteger("STUPIFY_LLAMA_UBATCH") ?? 512,
|
|
76
|
-
parallel: envInteger("STUPIFY_LLAMA_PARALLEL") ?? 2,
|
|
77
|
-
threads: envInteger("STUPIFY_LLAMA_THREADS"),
|
|
78
|
-
threadsBatch: envInteger("STUPIFY_LLAMA_THREADS_BATCH"),
|
|
79
|
-
flashAttention: envBoolean("STUPIFY_LLAMA_FLASH_ATTN"),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
async function runningServerModel(baseUrl) {
|
|
83
|
-
try {
|
|
84
|
-
const response = await fetch(`${baseUrl}/v1/models`, {
|
|
85
|
-
signal: AbortSignal.timeout(500),
|
|
86
|
-
});
|
|
87
|
-
if (!response.ok)
|
|
88
|
-
return null;
|
|
89
|
-
const body = (await response.json());
|
|
90
|
-
const id = body.data?.[0]?.id;
|
|
91
|
-
return typeof id === "string" ? id : null;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
async function ensureLlamaServerBinary() {
|
|
98
|
-
try {
|
|
99
|
-
await execFileAsync("llama-server", ["--version"], {
|
|
100
|
-
maxBuffer: 1024 * 1024,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
throw new Error(`Stupify needs llama-server for local inference.
|
|
105
|
-
Install llama.cpp first:
|
|
106
|
-
brew install llama.cpp`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
async function startLlamaServer(modelPath, modelId, modelName, runtime, ui) {
|
|
110
|
-
const logDir = path.join(cacheDir(), "logs");
|
|
111
|
-
await mkdir(logDir, { recursive: true });
|
|
112
|
-
const logPath = path.join(logDir, "llama-server.log");
|
|
113
|
-
const out = await open(logPath, "a");
|
|
114
|
-
const err = await open(logPath, "a");
|
|
115
|
-
ui.step(`Starting local model server: ${modelName}`);
|
|
116
|
-
ui.info(`llama-server log: ${logPath}`);
|
|
117
|
-
const args = [
|
|
118
|
-
"-m",
|
|
119
|
-
modelPath,
|
|
120
|
-
"-a",
|
|
121
|
-
modelId,
|
|
122
|
-
"--host",
|
|
123
|
-
LLAMA_SERVER_HOST,
|
|
124
|
-
"--port",
|
|
125
|
-
runtime.port,
|
|
126
|
-
"-c",
|
|
127
|
-
String(runtime.contextSize),
|
|
128
|
-
"--reasoning",
|
|
129
|
-
runtime.reasoning,
|
|
130
|
-
"--no-warmup",
|
|
131
|
-
];
|
|
132
|
-
if (runtime.gpuLayers !== undefined)
|
|
133
|
-
args.push("-ngl", String(runtime.gpuLayers));
|
|
134
|
-
if (runtime.batchSize !== undefined)
|
|
135
|
-
args.push("-b", String(runtime.batchSize));
|
|
136
|
-
if (runtime.ubatchSize !== undefined)
|
|
137
|
-
args.push("-ub", String(runtime.ubatchSize));
|
|
138
|
-
if (runtime.parallel !== undefined)
|
|
139
|
-
args.push("-np", String(runtime.parallel));
|
|
140
|
-
if (runtime.threads !== undefined)
|
|
141
|
-
args.push("-t", String(runtime.threads));
|
|
142
|
-
if (runtime.threadsBatch !== undefined)
|
|
143
|
-
args.push("-tb", String(runtime.threadsBatch));
|
|
144
|
-
if (runtime.flashAttention !== undefined)
|
|
145
|
-
args.push("-fa", runtime.flashAttention ? "on" : "off");
|
|
146
|
-
if (runtime.reasoningBudget !== undefined) {
|
|
147
|
-
args.push("--reasoning-budget", String(runtime.reasoningBudget));
|
|
148
|
-
}
|
|
149
|
-
const child = spawn("llama-server", args, {
|
|
150
|
-
detached: true,
|
|
151
|
-
stdio: ["ignore", out.fd, err.fd],
|
|
152
|
-
});
|
|
153
|
-
child.unref();
|
|
154
|
-
if (child.pid)
|
|
155
|
-
await writeFile(pidPath(runtime), String(child.pid));
|
|
156
|
-
await out.close();
|
|
157
|
-
await err.close();
|
|
158
|
-
}
|
|
159
|
-
async function stopManagedServer(runtime, ui) {
|
|
160
|
-
const pid = await managedServerPid(runtime);
|
|
161
|
-
if (!pid) {
|
|
162
|
-
const runningModel = await runningServerModel(runtime.baseUrl);
|
|
163
|
-
throw new Error(`A llama-server is already running with ${runningModel ?? "another model"}.
|
|
164
|
-
Stop it before switching models, or use STUPIFY_LLAMA_SERVER_URL for that server.`);
|
|
165
|
-
}
|
|
166
|
-
ui.step("Restarting local model server for selected model.");
|
|
167
|
-
try {
|
|
168
|
-
process.kill(pid, "SIGTERM");
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
await rm(pidPath(runtime), { force: true });
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const deadline = Date.now() + 15_000;
|
|
175
|
-
while (Date.now() < deadline) {
|
|
176
|
-
if (!(await runningServerModel(runtime.baseUrl))) {
|
|
177
|
-
await rm(pidPath(runtime), { force: true });
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
await sleep(250);
|
|
181
|
-
}
|
|
182
|
-
throw new Error("Timed out while stopping existing llama-server.");
|
|
183
|
-
}
|
|
184
|
-
async function managedServerPid(runtime) {
|
|
185
|
-
try {
|
|
186
|
-
const value = Number((await readFile(pidPath(runtime), "utf8")).trim());
|
|
187
|
-
return Number.isInteger(value) && value > 0 ? value : null;
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function pidPath(_runtime) {
|
|
194
|
-
return path.join(cacheDir(), "llama-server.pid");
|
|
195
|
-
}
|
|
196
|
-
function envInteger(name, fallback) {
|
|
197
|
-
const raw = process.env[name];
|
|
198
|
-
if (raw === undefined || raw === "")
|
|
199
|
-
return fallback;
|
|
200
|
-
const value = Number(raw);
|
|
201
|
-
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
202
|
-
}
|
|
203
|
-
function envBoolean(name) {
|
|
204
|
-
const raw = process.env[name];
|
|
205
|
-
if (raw === undefined || raw === "")
|
|
206
|
-
return undefined;
|
|
207
|
-
return /^(1|true|yes|on)$/i.test(raw);
|
|
208
|
-
}
|
|
209
|
-
async function waitForServer(baseUrl, modelId) {
|
|
210
|
-
const deadline = Date.now() + 120_000;
|
|
211
|
-
while (Date.now() < deadline) {
|
|
212
|
-
const runningModel = await runningServerModel(baseUrl);
|
|
213
|
-
if (runningModel === modelId)
|
|
214
|
-
return;
|
|
215
|
-
await sleep(500);
|
|
216
|
-
}
|
|
217
|
-
throw new Error(`llama-server did not become ready for ${modelId}.`);
|
|
218
|
-
}
|
|
219
|
-
function sleep(ms) {
|
|
220
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
221
|
-
}
|
|
222
|
-
async function downloadModel(modelPath, modelUrl, ui) {
|
|
223
|
-
const tempPath = `${modelPath}.download`;
|
|
224
|
-
await rm(tempPath, { force: true });
|
|
225
|
-
const downloadSpinner = ui.spinner("Downloading model");
|
|
226
|
-
let downloadProgress = null;
|
|
227
|
-
try {
|
|
228
|
-
const response = await fetch(modelUrl);
|
|
229
|
-
if (!response.ok || !response.body)
|
|
230
|
-
throw new Error(`Model download failed: HTTP ${response.status}`);
|
|
231
|
-
const total = Number(response.headers.get("content-length") ?? 0);
|
|
232
|
-
let received = 0;
|
|
233
|
-
let lastPrint = 0;
|
|
234
|
-
let lastProgressBytes = 0;
|
|
235
|
-
const reader = response.body.getReader();
|
|
236
|
-
const file = await open(tempPath, "wx");
|
|
237
|
-
if (total > 0) {
|
|
238
|
-
downloadSpinner.clear();
|
|
239
|
-
downloadProgress = ui.progress("Downloading model", total);
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
while (true) {
|
|
243
|
-
const { done, value } = await reader.read();
|
|
244
|
-
if (done)
|
|
245
|
-
break;
|
|
246
|
-
received += value.byteLength;
|
|
247
|
-
await file.write(value);
|
|
248
|
-
const now = Date.now();
|
|
249
|
-
if (total > 0 && now - lastPrint > 500) {
|
|
250
|
-
lastPrint = now;
|
|
251
|
-
downloadProgress?.advance(received - lastProgressBytes, `${formatBytes(received)} / ${formatBytes(total)}`);
|
|
252
|
-
lastProgressBytes = received;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
finally {
|
|
257
|
-
await file.close();
|
|
258
|
-
}
|
|
259
|
-
if (downloadProgress && received > lastProgressBytes) {
|
|
260
|
-
downloadProgress.advance(received - lastProgressBytes, `${formatBytes(received)} / ${formatBytes(total)}`);
|
|
261
|
-
}
|
|
262
|
-
const activeProgress = downloadProgress ?? downloadSpinner;
|
|
263
|
-
activeProgress.stop(total > 0
|
|
264
|
-
? `Downloaded ${formatBytes(received)} / ${formatBytes(total)}`
|
|
265
|
-
: "Downloaded model");
|
|
266
|
-
await rename(tempPath, modelPath);
|
|
267
|
-
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
const activeProgress = downloadProgress ?? downloadSpinner;
|
|
270
|
-
activeProgress.error("Model download failed");
|
|
271
|
-
await rm(tempPath, { force: true });
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
function cacheDir() {
|
|
276
|
-
if (process.env.STUPIFY_CACHE_DIR)
|
|
277
|
-
return process.env.STUPIFY_CACHE_DIR;
|
|
278
|
-
if (process.env.XDG_CACHE_HOME)
|
|
279
|
-
return path.join(process.env.XDG_CACHE_HOME, "stupify");
|
|
280
|
-
if (platform() === "darwin")
|
|
281
|
-
return path.join(homedir(), "Library", "Caches", "stupify");
|
|
282
|
-
if (platform() === "win32" && process.env.LOCALAPPDATA)
|
|
283
|
-
return path.join(process.env.LOCALAPPDATA, "stupify", "Cache");
|
|
284
|
-
return path.join(homedir(), ".cache", "stupify");
|
|
285
|
-
}
|
|
286
|
-
async function exists(filePath) {
|
|
287
|
-
try {
|
|
288
|
-
return (await stat(filePath)).isFile();
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
function formatBytes(bytes) {
|
|
295
|
-
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
296
|
-
}
|
package/dist/prompts.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { SemChangeSet, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
|
|
2
|
-
export declare function searchPrompt(input: Readonly<{
|
|
3
|
-
changeSet: SemChangeSet;
|
|
4
|
-
contexts: readonly SemContext[];
|
|
5
|
-
pack: SemContextPack;
|
|
6
|
-
patterns: readonly StupifyCheck[];
|
|
7
|
-
includeCounterReason: boolean;
|
|
8
|
-
}>): string;
|
package/dist/prompts.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
export function searchPrompt(input) {
|
|
2
|
-
return `You are Stupify's local search model.
|
|
3
|
-
Stupify checks whether AI-assisted coding may be replacing developer judgment.
|
|
4
|
-
You will receive:
|
|
5
|
-
1. Semantic changed entities selected by a fast local counter.
|
|
6
|
-
2. Compressed local file context from Repomix.
|
|
7
|
-
3. A list of search targets. Each target has exactly one assigned pattern.
|
|
8
|
-
|
|
9
|
-
Your job:
|
|
10
|
-
Evaluate each target only against its assigned pattern.
|
|
11
|
-
False positives are expensive.
|
|
12
|
-
Only emit a match if the assigned pattern clearly applies to that exact target.
|
|
13
|
-
Do not perform general code review.
|
|
14
|
-
Do not suggest improvements.
|
|
15
|
-
Do not choose a pattern.
|
|
16
|
-
Do not apply other patterns.
|
|
17
|
-
Do not report issues for unlisted targets.
|
|
18
|
-
Do not emit clean results.
|
|
19
|
-
Omitted target = clean.
|
|
20
|
-
Return JSON only:
|
|
21
|
-
{
|
|
22
|
-
"matches": [
|
|
23
|
-
{
|
|
24
|
-
"targetId": "t001",
|
|
25
|
-
"reason": "one sentence",
|
|
26
|
-
"proof": "short pointer"
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
Rules:
|
|
32
|
-
- Use only targetIds from the input.
|
|
33
|
-
- Emit at most 5 matches.
|
|
34
|
-
- Prefer omission over a weak match.
|
|
35
|
-
- Do not quote source code.
|
|
36
|
-
- Do not write generic feedback.
|
|
37
|
-
- Do not emit "no evidence" or "does not apply."
|
|
38
|
-
- Proof must point to concrete changed product code that implements the pattern.
|
|
39
|
-
- Proof must not be a file header or start with "diff --git".
|
|
40
|
-
- Do not use pattern registry text, prompt text, docs, tests, or examples as proof.
|
|
41
|
-
- Do not treat pattern or prompt wording as the code being evaluated.
|
|
42
|
-
- Do not treat plain conditionals, guard clauses, skip paths, or error handling as indirection.
|
|
43
|
-
- For unnecessary_complexity, identify the exact new named abstraction in proof.
|
|
44
|
-
- If unnecessary_complexity proof would only be a file, hunk, or conditional block, omit it.
|
|
45
|
-
- If nothing clearly matches, return { "matches": [] }.
|
|
46
|
-
|
|
47
|
-
SOURCE:
|
|
48
|
-
${input.changeSet.label}
|
|
49
|
-
|
|
50
|
-
SEARCH TARGETS:
|
|
51
|
-
${input.contexts.map((context) => formatSearchTarget(context, patternForContext(context, input.patterns), input.includeCounterReason)).join("\n\n") || "(none)"}
|
|
52
|
-
|
|
53
|
-
REPOMIX CONTEXT (${input.pack.filePaths.length} files, ${input.pack.totalTokens} tokens):
|
|
54
|
-
${input.pack.text || "(none)"}`;
|
|
55
|
-
}
|
|
56
|
-
function formatSearchPattern(check) {
|
|
57
|
-
return `Pattern: ${check.id} (${check.name})
|
|
58
|
-
Why this matters: ${check.why}
|
|
59
|
-
Question: ${check.searchPrompt ?? check.question}
|
|
60
|
-
Look for:
|
|
61
|
-
${check.lookFor.map((signal) => `- ${signal}`).join("\n")}
|
|
62
|
-
Ignore when:
|
|
63
|
-
${check.ignoreWhen.map((signal) => `- ${signal}`).join("\n")}
|
|
64
|
-
Match examples:
|
|
65
|
-
${(check.searchExamples?.match ?? check.examples?.match ?? []).map((example) => `- ${example}`).join("\n")}
|
|
66
|
-
Non-match examples:
|
|
67
|
-
${(check.searchExamples?.nonMatch ?? check.examples?.noMatch ?? []).map((example) => `- ${example}`).join("\n")}`;
|
|
68
|
-
}
|
|
69
|
-
function formatSearchTarget(context, pattern, includeCounterReason) {
|
|
70
|
-
return `TARGET ${context.targetId}
|
|
71
|
-
ASSIGNED ${formatSearchPattern(pattern)}
|
|
72
|
-
SEM TARGET:
|
|
73
|
-
ENTITY ${context.entityId}
|
|
74
|
-
NAME ${context.entityName}
|
|
75
|
-
KIND ${context.entityKind}
|
|
76
|
-
CHANGE ${context.changeKind}
|
|
77
|
-
FILE ${context.filePath ?? "(unknown)"}
|
|
78
|
-
${includeCounterReason ? `COUNTER_REASON ${context.reason}` : ""}`.trim();
|
|
79
|
-
}
|
|
80
|
-
function patternForContext(context, patterns) {
|
|
81
|
-
return patterns.find((pattern) => pattern.id === context.checkId) ?? {
|
|
82
|
-
id: context.checkId,
|
|
83
|
-
name: context.checkId,
|
|
84
|
-
question: `Does this target match ${context.checkId}?`,
|
|
85
|
-
why: "This pattern may indicate judgment-offload.",
|
|
86
|
-
lookFor: [],
|
|
87
|
-
ignoreWhen: [],
|
|
88
|
-
};
|
|
89
|
-
}
|
package/dist/render.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { SearchCommand, SearchRunJson } from "./types.ts";
|
|
2
|
-
import { type CliUi } from "./ui.ts";
|
|
3
|
-
export declare function renderSearchRun(run: SearchRunJson, command: SearchCommand): string;
|
|
4
|
-
export declare function renderSearchRunToUi(run: SearchRunJson, command: SearchCommand, ui: CliUi): void;
|
|
5
|
-
export declare function renderSearchHumanText(run: SearchRunJson, command: SearchCommand): string;
|
|
6
|
-
export declare function helpText(): string;
|
package/dist/render.js
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import { VERSION } from "./constants.js";
|
|
2
|
-
import { format } from "./ui.js";
|
|
3
|
-
export function renderSearchRun(run, command) {
|
|
4
|
-
if (command.json)
|
|
5
|
-
return JSON.stringify(run, null, 2);
|
|
6
|
-
return renderSearchHumanText(run, command);
|
|
7
|
-
}
|
|
8
|
-
export function renderSearchRunToUi(run, command, ui) {
|
|
9
|
-
if (command.json) {
|
|
10
|
-
ui.writeStdout(renderSearchRun(run, command));
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
if (run.stats.skipped && run.stats.skipReason === "input_too_large") {
|
|
14
|
-
ui.warn("Search skipped: input is too large for precise local search.");
|
|
15
|
-
ui.note(oversizedText(run, command), "Skipped");
|
|
16
|
-
ui.outro("Warn-only. Nothing blocked.");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
|
|
20
|
-
ui.success("Search complete: no search targets found.");
|
|
21
|
-
ui.note(cleanSummaryText(run), "Summary");
|
|
22
|
-
ui.outro("No judgment-offload signals found.");
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
if (run.matches.length === 0) {
|
|
26
|
-
ui.success("Search complete: no judgment-offload signals found.");
|
|
27
|
-
ui.note(cleanSummaryText(run), "Summary");
|
|
28
|
-
ui.outro("Warn-only. Nothing blocked.");
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
ui.warn(format.warn(format.heading("AI SLOP DETECTED")));
|
|
32
|
-
ui.note(matchSummaryText(run, command), "Summary");
|
|
33
|
-
for (const group of groupMatchesByFile(run.matches)) {
|
|
34
|
-
ui.note(renderMatchGroup(group, run), group.filePath);
|
|
35
|
-
}
|
|
36
|
-
ui.outro(summaryLine(run));
|
|
37
|
-
}
|
|
38
|
-
export function renderSearchHumanText(run, command) {
|
|
39
|
-
if (run.stats.skipped && run.stats.skipReason === "input_too_large") {
|
|
40
|
-
return `${format.heading("Search skipped")}
|
|
41
|
-
${oversizedText(run, command)}
|
|
42
|
-
Warn-only. Nothing blocked.`;
|
|
43
|
-
}
|
|
44
|
-
if (run.stats.skipped && run.stats.skipReason === "no_candidates") {
|
|
45
|
-
return `${format.heading("Search complete.")}
|
|
46
|
-
${format.label("Patterns:")} ${run.patterns.join(", ")}
|
|
47
|
-
${format.success("No search targets found.")}`;
|
|
48
|
-
}
|
|
49
|
-
if (run.matches.length === 0) {
|
|
50
|
-
return `${format.heading("Search complete.")}
|
|
51
|
-
${format.label("Patterns:")} ${run.patterns.join(", ")}
|
|
52
|
-
${format.success("No judgment-offload signals found.")}`;
|
|
53
|
-
}
|
|
54
|
-
const groups = groupMatchesByFile(run.matches);
|
|
55
|
-
return `${slopHeading()}
|
|
56
|
-
${matchSummaryText(run, command)}
|
|
57
|
-
|
|
58
|
-
${groups.map((group) => `${format.heading(group.filePath)}
|
|
59
|
-
${renderMatchGroup(group, run)}`).join("\n\n")}
|
|
60
|
-
${format.muted(summaryLine(run))}`;
|
|
61
|
-
}
|
|
62
|
-
export function helpText() {
|
|
63
|
-
return `Stupify ${VERSION}
|
|
64
|
-
|
|
65
|
-
Usage:
|
|
66
|
-
stupify
|
|
67
|
-
stupify --since "2 weeks ago"
|
|
68
|
-
stupify --commit <commit>
|
|
69
|
-
stupify --commits <count>
|
|
70
|
-
stupify --staged
|
|
71
|
-
stupify --mode search --staged
|
|
72
|
-
stupify hook install|uninstall|status
|
|
73
|
-
stupify doctor
|
|
74
|
-
stupify bench search experiments/search-bench.json
|
|
75
|
-
git diff HEAD~1..HEAD | stupify --stdin
|
|
76
|
-
|
|
77
|
-
Options:
|
|
78
|
-
--staged Search staged changes.
|
|
79
|
-
--mode <mode> search. Search is the only analysis mode.
|
|
80
|
-
--since <date> Search the net diff from the first commit before this git date to HEAD.
|
|
81
|
-
--commit <commit> Search one commit as a net diff.
|
|
82
|
-
--commits <count> Search the net diff across the last N non-merge commits.
|
|
83
|
-
--stdin Read a git diff from stdin.
|
|
84
|
-
--debug-sem Print sem commands and stderr.
|
|
85
|
-
--max-candidates <n> Max semantic search targets. Default: 50.
|
|
86
|
-
--max-search-input-tokens <n>
|
|
87
|
-
Max search input tokens before skipping. Default: 12000.
|
|
88
|
-
--checks <ids> Comma-separated pattern ids.
|
|
89
|
-
--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.
|
|
90
|
-
--search-profile <path>
|
|
91
|
-
Dev/bench-only search profile override.
|
|
92
|
-
--include-counter-reason-in-prompt
|
|
93
|
-
Debug/bench-only: include counter reason in the model prompt.
|
|
94
|
-
--json Print JSON only.
|
|
95
|
-
|
|
96
|
-
Diagnostics:
|
|
97
|
-
stupify doctor Check local setup, hook status, and privacy boundary.
|
|
98
|
-
|
|
99
|
-
Default:
|
|
100
|
-
stupify is equivalent to stupify --since "2 weeks ago".
|
|
101
|
-
|
|
102
|
-
Pipeline:
|
|
103
|
-
sem diff -> counter scout -> Repomix context -> local search model.
|
|
104
|
-
|
|
105
|
-
Not included:
|
|
106
|
-
Findings audit, validators, judges, baselines, sharing, hosted server calls, GitHub, dashboards, or repo-wide crawling.
|
|
107
|
-
`;
|
|
108
|
-
}
|
|
109
|
-
function oversizedText(run, command) {
|
|
110
|
-
const targetLimit = Math.max((run.stats.inputTokens ?? 12_000) + 1, (run.stats.inputTokenCap ?? 12_000) * 2);
|
|
111
|
-
return [
|
|
112
|
-
`Size: ~${run.stats.inputTokens ?? "unknown"} tokens`,
|
|
113
|
-
`Limit: ${run.stats.inputTokenCap ?? "unknown"} tokens`,
|
|
114
|
-
"Stupify skipped the search rather than review truncated context.",
|
|
115
|
-
`Try: ${sourceHint(command)} --max-search-input-tokens ${targetLimit}`,
|
|
116
|
-
].join("\n");
|
|
117
|
-
}
|
|
118
|
-
function cleanSummaryText(run) {
|
|
119
|
-
return [
|
|
120
|
-
`Patterns: ${run.patterns.join(", ")}`,
|
|
121
|
-
run.stats.filesChanged === undefined ? null : `Diff: ${run.stats.filesChanged} files, ${run.stats.entitiesScanned ?? 0} changed entities`,
|
|
122
|
-
].filter(Boolean).join("\n");
|
|
123
|
-
}
|
|
124
|
-
function matchSummaryText(run, command) {
|
|
125
|
-
const fileCount = groupMatchesByFile(run.matches).length;
|
|
126
|
-
const fileNoun = fileCount === 1 ? "file" : "files";
|
|
127
|
-
return [
|
|
128
|
-
`${run.matches.length} ${signalNoun(run.matches.length)} across ${fileCount} ${fileNoun}`,
|
|
129
|
-
`${committerLabel(run)} · ${sourceLabel(command)}`,
|
|
130
|
-
"Warn-only. Nothing blocked.",
|
|
131
|
-
"",
|
|
132
|
-
patternSummaryLine(run),
|
|
133
|
-
].filter((line) => line !== null).join("\n");
|
|
134
|
-
}
|
|
135
|
-
function patternSummaryLine(run) {
|
|
136
|
-
const counts = new Map();
|
|
137
|
-
for (const match of run.matches)
|
|
138
|
-
counts.set(patternLabel(match), (counts.get(patternLabel(match)) ?? 0) + 1);
|
|
139
|
-
return [...counts.entries()].map(([patternName, count]) => `${patternName} ${count}`).join(" · ");
|
|
140
|
-
}
|
|
141
|
-
function groupMatchesByFile(matches) {
|
|
142
|
-
const groups = new Map();
|
|
143
|
-
for (const match of matches) {
|
|
144
|
-
const filePath = proofFilePath(match.proof);
|
|
145
|
-
const group = groups.get(filePath) ?? [];
|
|
146
|
-
group.push(match);
|
|
147
|
-
groups.set(filePath, group);
|
|
148
|
-
}
|
|
149
|
-
return [...groups.entries()].map(([filePath, groupedMatches]) => ({
|
|
150
|
-
filePath,
|
|
151
|
-
matches: groupedMatches,
|
|
152
|
-
}));
|
|
153
|
-
}
|
|
154
|
-
function renderMatchGroup(group, run) {
|
|
155
|
-
return group.matches.map((match, index) => {
|
|
156
|
-
const lines = [
|
|
157
|
-
matchHeadline(match, run, index),
|
|
158
|
-
match.reason,
|
|
159
|
-
match.snapshot ? `\n\`\`\`\n${match.snapshot}\n\`\`\`` : null,
|
|
160
|
-
format.muted(`${proofDetail(match.proof)}${commitSubjectSuffix(run)}`),
|
|
161
|
-
match.checkWhy ?? "This pattern may indicate judgment-offload.",
|
|
162
|
-
];
|
|
163
|
-
return lines.filter(Boolean).join("\n");
|
|
164
|
-
}).join("\n\n");
|
|
165
|
-
}
|
|
166
|
-
function matchHeadline(match, run, index) {
|
|
167
|
-
return `${index + 1}. ${format.label(patternLabel(match))}: ${headlineArgs(match)} -- ${matchBlameLabel(match, run)}`;
|
|
168
|
-
}
|
|
169
|
-
function patternLabel(match) {
|
|
170
|
-
return titleCase(match.patternName ?? match.patternId.replace(/_/g, " "));
|
|
171
|
-
}
|
|
172
|
-
function headlineArgs(match) {
|
|
173
|
-
const destination = entityNameFromProof(match.proof);
|
|
174
|
-
const source = firstBacktickedToken(match.reason) ?? firstLikelySource(match.reason, destination);
|
|
175
|
-
if (source && destination && source !== destination)
|
|
176
|
-
return `${codeLabel(source)} -> ${codeLabel(destination)}`;
|
|
177
|
-
if (destination)
|
|
178
|
-
return codeLabel(destination);
|
|
179
|
-
return codeLabel(match.targetId);
|
|
180
|
-
}
|
|
181
|
-
function matchBlameLabel(match, run) {
|
|
182
|
-
return match.blame ? blameSummaryLabel(match.blame) : runLevelBlameLabel(run);
|
|
183
|
-
}
|
|
184
|
-
function blameSummaryLabel(blame) {
|
|
185
|
-
return `${blame.author} (${blame.subject})`;
|
|
186
|
-
}
|
|
187
|
-
function runLevelBlameLabel(run) {
|
|
188
|
-
const author = committerLabel(run);
|
|
189
|
-
const subject = firstHumanSubject(run.stats.commitSubjects ?? []);
|
|
190
|
-
return subject ? `${author} (${subject})` : author;
|
|
191
|
-
}
|
|
192
|
-
function commitSubjectSuffix(run) {
|
|
193
|
-
const subject = firstHumanSubject(run.stats.commitSubjects ?? []);
|
|
194
|
-
return subject ? ` · commit: ${subject}` : "";
|
|
195
|
-
}
|
|
196
|
-
function firstHumanSubject(subjects) {
|
|
197
|
-
return subjects.map((subject) => subject.trim()).find(Boolean);
|
|
198
|
-
}
|
|
199
|
-
function codeLabel(value) {
|
|
200
|
-
return `\`${value}\``;
|
|
201
|
-
}
|
|
202
|
-
function titleCase(value) {
|
|
203
|
-
return value.replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
|
|
204
|
-
}
|
|
205
|
-
function entityNameFromProof(proof) {
|
|
206
|
-
const parts = proof.split("::");
|
|
207
|
-
return parts[2] || parts[1] || undefined;
|
|
208
|
-
}
|
|
209
|
-
function firstBacktickedToken(value) {
|
|
210
|
-
const match = /`([^`]+)`/.exec(value);
|
|
211
|
-
return cleanToken(match?.[1]);
|
|
212
|
-
}
|
|
213
|
-
function firstLikelySource(value, destination) {
|
|
214
|
-
const tokens = [...value.matchAll(/\b[A-Z][A-Za-z0-9_]*(?:\[[^\]]+\])?\b/g)]
|
|
215
|
-
.map((match) => cleanToken(match[0]))
|
|
216
|
-
.filter((token) => Boolean(token));
|
|
217
|
-
return tokens.find((token) => token !== destination && token !== "The");
|
|
218
|
-
}
|
|
219
|
-
function cleanToken(value) {
|
|
220
|
-
const token = value?.trim().replace(/[.,;:]+$/, "");
|
|
221
|
-
return token || undefined;
|
|
222
|
-
}
|
|
223
|
-
function proofFilePath(proof) {
|
|
224
|
-
return proof.split("::")[0] || proof;
|
|
225
|
-
}
|
|
226
|
-
function proofDetail(proof) {
|
|
227
|
-
const [, ...rest] = proof.split("::");
|
|
228
|
-
return rest.length > 0 ? `::${rest.join("::")}` : proof;
|
|
229
|
-
}
|
|
230
|
-
function sourceHint(command) {
|
|
231
|
-
if (command.kind === "staged")
|
|
232
|
-
return "--staged";
|
|
233
|
-
if (command.kind === "since")
|
|
234
|
-
return `--since "${command.since}"`;
|
|
235
|
-
if (command.kind === "commit")
|
|
236
|
-
return `--commit ${command.commit}`;
|
|
237
|
-
if (command.kind === "commits")
|
|
238
|
-
return `--commits ${command.count}`;
|
|
239
|
-
return "--stdin";
|
|
240
|
-
}
|
|
241
|
-
function sourceLabel(command) {
|
|
242
|
-
if (command.kind === "staged")
|
|
243
|
-
return "staged";
|
|
244
|
-
if (command.kind === "since")
|
|
245
|
-
return sinceLabel(command.since);
|
|
246
|
-
if (command.kind === "commit")
|
|
247
|
-
return `commit ${command.commit}`;
|
|
248
|
-
if (command.kind === "commits")
|
|
249
|
-
return `last ${command.count} commits`;
|
|
250
|
-
return "stdin";
|
|
251
|
-
}
|
|
252
|
-
function committerLabel(run) {
|
|
253
|
-
const committers = humanCommitters(run.stats.committers ?? []).map(committerDisplayName);
|
|
254
|
-
if (committers.length === 0)
|
|
255
|
-
return "unknown committer";
|
|
256
|
-
if (committers.length <= 3)
|
|
257
|
-
return committers.join(", ");
|
|
258
|
-
return `${committers.slice(0, 3).join(", ")} +${committers.length - 3} more`;
|
|
259
|
-
}
|
|
260
|
-
function humanCommitters(committers) {
|
|
261
|
-
const nonEmpty = committers.filter(Boolean);
|
|
262
|
-
const humans = nonEmpty.filter((committer) => !isBotCommitter(committer));
|
|
263
|
-
return humans.length > 0 ? humans : nonEmpty;
|
|
264
|
-
}
|
|
265
|
-
function isBotCommitter(value) {
|
|
266
|
-
return /(?:^|<)(?:github|dependabot|renovate)(?:\s|@|>)/i.test(value) ||
|
|
267
|
-
/(?:noreply@github\.com|bot@)/i.test(value);
|
|
268
|
-
}
|
|
269
|
-
function committerDisplayName(value) {
|
|
270
|
-
return value.replace(/\s*<[^>]+>\s*$/, "").trim() || value;
|
|
271
|
-
}
|
|
272
|
-
function slopHeading() {
|
|
273
|
-
const heading = "AI SLOP DETECTED";
|
|
274
|
-
return `${format.warn(format.heading(heading))}
|
|
275
|
-
${format.warn("=".repeat(heading.length))}`;
|
|
276
|
-
}
|
|
277
|
-
function sinceLabel(since) {
|
|
278
|
-
const value = since.trim().toLowerCase();
|
|
279
|
-
if (value === "yesterday" || value === "1 day ago")
|
|
280
|
-
return "yesterday";
|
|
281
|
-
const match = /^(\d+)\s+(day|week|month|year)s?\s+ago$/.exec(value);
|
|
282
|
-
if (!match)
|
|
283
|
-
return `since ${since}`;
|
|
284
|
-
const count = Number(match[1]);
|
|
285
|
-
const unit = match[2];
|
|
286
|
-
if (count === 1)
|
|
287
|
-
return `last ${unit}`;
|
|
288
|
-
return `last ${count} ${unit}s`;
|
|
289
|
-
}
|
|
290
|
-
function summaryLine(run) {
|
|
291
|
-
return `${run.matches.length} ${signalNoun(run.matches.length)}. Warn-only. Nothing blocked.`;
|
|
292
|
-
}
|
|
293
|
-
function signalNoun(count) {
|
|
294
|
-
return count === 1 ? "signal" : "signals";
|
|
295
|
-
}
|