@stupify/cli 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis.js +5 -1
- package/dist/checks.js +11 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/counter-scout.js +1 -1
- package/dist/prompts.js +2 -0
- package/dist/render.js +2 -1
- package/dist/stupify.js +123 -36
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/analysis.ts +6 -1
- package/src/checks.ts +11 -1
- package/src/constants.ts +1 -1
- package/src/counter-scout.ts +1 -1
- package/src/prompts.ts +2 -0
- package/src/render.ts +2 -1
- package/src/stupify.ts +184 -48
- package/src/types.ts +4 -0
package/dist/analysis.js
CHANGED
|
@@ -71,10 +71,14 @@ function uncheckedSearchMatches(value, contexts) {
|
|
|
71
71
|
targetId,
|
|
72
72
|
patternId: context.checkId,
|
|
73
73
|
reason: match.reason ?? "",
|
|
74
|
-
proof:
|
|
74
|
+
proof: sourcePointer(context),
|
|
75
75
|
}];
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
+
function sourcePointer(context) {
|
|
79
|
+
const file = context.filePath ?? "(unknown)";
|
|
80
|
+
return `${file}::${context.entityKind || "entity"}::${context.entityName || context.entityId}`;
|
|
81
|
+
}
|
|
78
82
|
async function runJsonPrompt(model, prompt, schema, temperature) {
|
|
79
83
|
return cachedJson("model-json", fingerprint({
|
|
80
84
|
version: 1,
|
package/dist/checks.js
CHANGED
|
@@ -4,6 +4,7 @@ export const defaultChecks = [
|
|
|
4
4
|
id: checkId("duplicated_schema"),
|
|
5
5
|
name: "Duplicated schema",
|
|
6
6
|
question: "Did the change duplicate an existing type, schema, payload, or DTO shape?",
|
|
7
|
+
why: "Duplicated shapes make it easier for AI-assisted changes to drift away from the real source of truth.",
|
|
7
8
|
lookFor: [
|
|
8
9
|
"local shape mirrors existing fields and maps them one-for-one",
|
|
9
10
|
"new response, payload, schema, or DTO adds no filtering, renaming, validation, or versioning",
|
|
@@ -28,6 +29,7 @@ export const defaultChecks = [
|
|
|
28
29
|
id: checkId("unnecessary_complexity"),
|
|
29
30
|
name: "Unnecessary complexity",
|
|
30
31
|
question: "Did the change add structure without buying clarity?",
|
|
32
|
+
why: "Extra indirection can hide simple decisions and make the code feel more designed than understood.",
|
|
31
33
|
lookFor: [
|
|
32
34
|
"helper, wrapper, service, layer, or extra file around simple logic without reuse",
|
|
33
35
|
],
|
|
@@ -68,6 +70,7 @@ Prefer no match over a weak match.`,
|
|
|
68
70
|
id: checkId("fake_precision_windowing"),
|
|
69
71
|
name: "Fake precision windowing",
|
|
70
72
|
question: "Did the change add fake precision around model context?",
|
|
73
|
+
why: "Precise-looking bookkeeping can create confidence without improving the actual behavior.",
|
|
71
74
|
lookFor: [
|
|
72
75
|
"precise-looking counts, budgets, ratios, reports, or batching fields without useful behavior",
|
|
73
76
|
],
|
|
@@ -80,6 +83,7 @@ Prefer no match over a weak match.`,
|
|
|
80
83
|
id: checkId("coauthored_slop"),
|
|
81
84
|
name: "Coauthored slop",
|
|
82
85
|
question: "Does author metadata contain co-author text?",
|
|
86
|
+
why: "Careless metadata is a cheap signal that the change may not have been reviewed with intent.",
|
|
83
87
|
lookFor: [
|
|
84
88
|
"author signal contains coauhtoried, coauthored, or co-authored text",
|
|
85
89
|
],
|
|
@@ -91,6 +95,7 @@ Prefer no match over a weak match.`,
|
|
|
91
95
|
id: checkId("mega_file"),
|
|
92
96
|
name: "Mega file",
|
|
93
97
|
question: "Is a touched non-config file over 1000 LOC?",
|
|
98
|
+
why: "Large files make judgment harder by concentrating unrelated decisions in one place.",
|
|
94
99
|
lookFor: [
|
|
95
100
|
"touched non-config source file over 1000 LOC",
|
|
96
101
|
],
|
|
@@ -102,6 +107,7 @@ Prefer no match over a weak match.`,
|
|
|
102
107
|
id: checkId("over_commenting"),
|
|
103
108
|
name: "Over commenting",
|
|
104
109
|
question: "Did the change add noisy comments?",
|
|
110
|
+
why: "Narrative comments can make routine code look deliberate without clarifying the underlying tradeoff.",
|
|
105
111
|
lookFor: [
|
|
106
112
|
"comments restate obvious code or narrate simple logic",
|
|
107
113
|
],
|
|
@@ -129,6 +135,7 @@ Prefer no match over a weak match.`,
|
|
|
129
135
|
id: checkId("lint_bypass"),
|
|
130
136
|
name: "Lint bypass",
|
|
131
137
|
question: "Did the change bypass lint or type rules?",
|
|
138
|
+
why: "Unexplained suppressions remove useful feedback exactly where a change needs more scrutiny.",
|
|
132
139
|
lookFor: [
|
|
133
140
|
"adds suppressions, any, broad casts, or weakens lint/typecheck config",
|
|
134
141
|
],
|
|
@@ -152,6 +159,7 @@ Prefer no match over a weak match.`,
|
|
|
152
159
|
id: checkId("inconsistent_patterns"),
|
|
153
160
|
name: "Inconsistent patterns",
|
|
154
161
|
question: "Does the change clash with nearby patterns?",
|
|
162
|
+
why: "Pattern drift can signal that a change followed generic suggestions instead of local codebase judgment.",
|
|
155
163
|
lookFor: [
|
|
156
164
|
"same job uses different naming, errors, state, imports, or layout than nearby files",
|
|
157
165
|
],
|
|
@@ -164,6 +172,7 @@ Prefer no match over a weak match.`,
|
|
|
164
172
|
id: checkId("reinvented_utils"),
|
|
165
173
|
name: "Reinvented utils",
|
|
166
174
|
question: "Did the change recreate an existing utility?",
|
|
175
|
+
why: "Generic helper reinvention can be a sign that the change optimized for plausible code over local reuse.",
|
|
167
176
|
lookFor: [
|
|
168
177
|
"new helper duplicates local utility or standard library behavior",
|
|
169
178
|
],
|
|
@@ -173,7 +182,7 @@ Prefer no match over a weak match.`,
|
|
|
173
182
|
"helper is domain-specific or used by multiple local call sites",
|
|
174
183
|
],
|
|
175
184
|
hookMode: "warn",
|
|
176
|
-
searchPrompt: "Find only tiny generic utility functions that recreate common helpers such as clamp, debounce, throttle, slugify,
|
|
185
|
+
searchPrompt: "Find only tiny generic utility functions that recreate common helpers such as clamp, debounce, throttle, slugify, sort, pick, omit, uniq, or shuffle without domain-specific behavior. Do not match group/resolve/parse/format helpers, domain formatting, feature constants, or helpers with multiple obvious call sites.",
|
|
177
186
|
searchExamples: {
|
|
178
187
|
match: [
|
|
179
188
|
"clampValue returns min, max, or value.",
|
|
@@ -188,6 +197,7 @@ Prefer no match over a weak match.`,
|
|
|
188
197
|
id: checkId("operator_style_mismatch"),
|
|
189
198
|
name: "Operator style mismatch",
|
|
190
199
|
question: "Does the change read unlike the surrounding code?",
|
|
200
|
+
why: "Style mismatch can reveal generic generated code that was not reconciled with nearby conventions.",
|
|
191
201
|
lookFor: [
|
|
192
202
|
"generic or template-like names, abstractions, comments, or control flow clash with local style",
|
|
193
203
|
],
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
package/dist/counter-scout.js
CHANGED
|
@@ -120,7 +120,7 @@ function lintBypassSignal(value) {
|
|
|
120
120
|
}
|
|
121
121
|
function reinventedUtilitySignal(change) {
|
|
122
122
|
const name = change.entityName;
|
|
123
|
-
if (!/^(clamp|debounce|throttle|slug|slugify|
|
|
123
|
+
if (!/^(clamp|debounce|throttle|slug|slugify|sort|shuffle|memoize|pick|omit|uniq)/i.test(name))
|
|
124
124
|
return false;
|
|
125
125
|
const content = change.afterContent ?? "";
|
|
126
126
|
if (/currency|invoice|refund|subscription|tier|domain/i.test(`${name}\n${content}`))
|
package/dist/prompts.js
CHANGED
|
@@ -55,6 +55,7 @@ ${input.pack.text || "(none)"}`;
|
|
|
55
55
|
}
|
|
56
56
|
function formatSearchPattern(check) {
|
|
57
57
|
return `Pattern: ${check.id} (${check.name})
|
|
58
|
+
Why this matters: ${check.why}
|
|
58
59
|
Question: ${check.searchPrompt ?? check.question}
|
|
59
60
|
Look for:
|
|
60
61
|
${check.lookFor.map((signal) => `- ${signal}`).join("\n")}
|
|
@@ -81,6 +82,7 @@ function patternForContext(context, patterns) {
|
|
|
81
82
|
id: context.checkId,
|
|
82
83
|
name: context.checkId,
|
|
83
84
|
question: `Does this target match ${context.checkId}?`,
|
|
85
|
+
why: "This pattern may indicate judgment-offload.",
|
|
84
86
|
lookFor: [],
|
|
85
87
|
ignoreWhen: [],
|
|
86
88
|
};
|
package/dist/render.js
CHANGED
|
@@ -29,7 +29,8 @@ No judgment-offload signals found.`;
|
|
|
29
29
|
return `🧙 stupify 🪄
|
|
30
30
|
Possible judgment-offload detected:
|
|
31
31
|
${run.matches.map((match, index) => `${index + 1}. ${match.patternId}
|
|
32
|
-
${match.
|
|
32
|
+
Why: ${match.checkWhy ?? "This pattern may indicate judgment-offload."}
|
|
33
|
+
Match: ${match.reason}
|
|
33
34
|
Proof: ${match.proof}`).join("\n")}
|
|
34
35
|
Search mode is warn-only.`;
|
|
35
36
|
}
|
package/dist/stupify.js
CHANGED
|
@@ -133,9 +133,18 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
133
133
|
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
134
134
|
? initialPack
|
|
135
135
|
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
const batches = await buildSearchBatches({
|
|
137
|
+
command,
|
|
138
|
+
changeSet,
|
|
139
|
+
contexts: searchContexts,
|
|
140
|
+
initialPack: pack,
|
|
141
|
+
checks,
|
|
142
|
+
profile,
|
|
143
|
+
includeCounterReasonInPrompt: command.includeCounterReasonInPrompt,
|
|
144
|
+
maxSearchInputTokens,
|
|
145
|
+
baseRepomixConfig,
|
|
146
|
+
});
|
|
147
|
+
if (batches.batches.length === 0) {
|
|
139
148
|
return {
|
|
140
149
|
schemaVersion: "search.v1",
|
|
141
150
|
mode: "search",
|
|
@@ -145,7 +154,7 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
145
154
|
stats: {
|
|
146
155
|
elapsedMs: Date.now() - startedAt,
|
|
147
156
|
modelCalls: 0,
|
|
148
|
-
inputTokens: estimatedInputTokens,
|
|
157
|
+
inputTokens: batches.estimatedInputTokens,
|
|
149
158
|
inputTokenCap: maxSearchInputTokens,
|
|
150
159
|
skipped: true,
|
|
151
160
|
skipReason: "input_too_large",
|
|
@@ -156,6 +165,8 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
156
165
|
repomixFiles: pack.filePaths.length,
|
|
157
166
|
repomixTokens: pack.totalTokens,
|
|
158
167
|
repomixConfig: pack.config,
|
|
168
|
+
searchBatches: 0,
|
|
169
|
+
skippedTargets: batches.skippedTargets,
|
|
159
170
|
profileId: profile?.id,
|
|
160
171
|
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
161
172
|
targetsPreview: previewTargets(searchContexts),
|
|
@@ -163,38 +174,33 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
163
174
|
matches: [],
|
|
164
175
|
};
|
|
165
176
|
}
|
|
177
|
+
if (batches.wasSplit && !command.json) {
|
|
178
|
+
console.error(`Search input is large; queued ${batches.batches.length} smaller search batches.`);
|
|
179
|
+
if (batches.skippedTargets > 0) {
|
|
180
|
+
console.error(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
166
183
|
const modelPath = await firstRunModelBootstrap(command.model);
|
|
167
184
|
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
candidates: contexts.length,
|
|
186
|
-
searchTargets: searchContexts.length,
|
|
187
|
-
repomixFiles: pack.filePaths.length,
|
|
188
|
-
repomixTokens: pack.totalTokens,
|
|
189
|
-
repomixConfig: pack.config,
|
|
190
|
-
profileId: profile?.id,
|
|
191
|
-
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
192
|
-
targetsPreview: previewTargets(searchContexts),
|
|
193
|
-
},
|
|
194
|
-
matches: [],
|
|
195
|
-
};
|
|
185
|
+
const matches = [];
|
|
186
|
+
let modelCalls = 0;
|
|
187
|
+
let inputTokens = 0;
|
|
188
|
+
let exactSkippedTargets = batches.skippedTargets;
|
|
189
|
+
for (const batch of batches.batches) {
|
|
190
|
+
const batchInputTokens = await countPromptTokens(model, batch.request.prompt);
|
|
191
|
+
inputTokens += batchInputTokens;
|
|
192
|
+
if (batchInputTokens > maxSearchInputTokens) {
|
|
193
|
+
exactSkippedTargets += batch.contexts.length;
|
|
194
|
+
if (!command.json) {
|
|
195
|
+
console.error(`Skipped ${batch.contexts.length} targets after exact token count exceeded the limit.`);
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const { value } = await t.trace("search.model", () => runSearch(model, batch.request), { count: (v) => v.length });
|
|
200
|
+
modelCalls += 1;
|
|
201
|
+
matches.push(...withCheckWhy(value, checks));
|
|
196
202
|
}
|
|
197
|
-
const
|
|
203
|
+
const uniqueMatches = dedupeMatches(matches);
|
|
198
204
|
return {
|
|
199
205
|
schemaVersion: "search.v1",
|
|
200
206
|
mode: "search",
|
|
@@ -203,7 +209,7 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
203
209
|
patterns: patternIds,
|
|
204
210
|
stats: {
|
|
205
211
|
elapsedMs: Date.now() - startedAt,
|
|
206
|
-
modelCalls
|
|
212
|
+
modelCalls,
|
|
207
213
|
inputTokens,
|
|
208
214
|
inputTokenCap: maxSearchInputTokens,
|
|
209
215
|
filesChanged: changeSet.summary.fileCount,
|
|
@@ -213,17 +219,98 @@ export async function runSearchCommand(command, startedAt) {
|
|
|
213
219
|
repomixFiles: pack.filePaths.length,
|
|
214
220
|
repomixTokens: pack.totalTokens,
|
|
215
221
|
repomixConfig: pack.config,
|
|
222
|
+
searchBatches: batches.batches.length,
|
|
223
|
+
skippedTargets: exactSkippedTargets,
|
|
216
224
|
profileId: profile?.id,
|
|
217
225
|
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
218
226
|
targetsPreview: previewTargets(searchContexts),
|
|
219
227
|
},
|
|
220
|
-
matches,
|
|
228
|
+
matches: uniqueMatches,
|
|
221
229
|
};
|
|
222
230
|
}
|
|
223
231
|
finally {
|
|
224
232
|
await changeSet.cleanup();
|
|
225
233
|
}
|
|
226
234
|
}
|
|
235
|
+
function dedupeMatches(matches) {
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
return matches.filter((match) => {
|
|
238
|
+
const key = `${match.patternId}\n${match.proof.trim()}`;
|
|
239
|
+
if (seen.has(key))
|
|
240
|
+
return false;
|
|
241
|
+
seen.add(key);
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function withCheckWhy(matches, checks) {
|
|
246
|
+
const checksById = new Map(checks.map((check) => [check.id, check]));
|
|
247
|
+
return matches.map((match) => ({
|
|
248
|
+
...match,
|
|
249
|
+
checkWhy: checksById.get(match.patternId)?.why,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
async function buildSearchBatches(input) {
|
|
253
|
+
const first = makeSearchBatch(input, input.contexts, input.initialPack);
|
|
254
|
+
if (first.estimatedInputTokens <= input.maxSearchInputTokens) {
|
|
255
|
+
return {
|
|
256
|
+
batches: [first],
|
|
257
|
+
estimatedInputTokens: first.estimatedInputTokens,
|
|
258
|
+
skippedTargets: 0,
|
|
259
|
+
wasSplit: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const batches = [];
|
|
263
|
+
let skippedTargets = 0;
|
|
264
|
+
let currentContexts = [];
|
|
265
|
+
let currentBatch = null;
|
|
266
|
+
for (const context of input.contexts) {
|
|
267
|
+
const candidateContexts = [...currentContexts, context];
|
|
268
|
+
const candidateBatch = await makeSearchBatchWithPack(input, candidateContexts);
|
|
269
|
+
if (candidateBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
|
|
270
|
+
currentContexts = candidateContexts;
|
|
271
|
+
currentBatch = candidateBatch;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (currentBatch) {
|
|
275
|
+
batches.push(currentBatch);
|
|
276
|
+
currentContexts = [];
|
|
277
|
+
currentBatch = null;
|
|
278
|
+
}
|
|
279
|
+
const singleBatch = candidateContexts.length === 1
|
|
280
|
+
? candidateBatch
|
|
281
|
+
: await makeSearchBatchWithPack(input, [context]);
|
|
282
|
+
if (singleBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
|
|
283
|
+
currentContexts = [context];
|
|
284
|
+
currentBatch = singleBatch;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
skippedTargets += 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (currentBatch)
|
|
291
|
+
batches.push(currentBatch);
|
|
292
|
+
return {
|
|
293
|
+
batches,
|
|
294
|
+
estimatedInputTokens: first.estimatedInputTokens,
|
|
295
|
+
skippedTargets,
|
|
296
|
+
wasSplit: true,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function makeSearchBatch(input, contexts, pack) {
|
|
300
|
+
const request = buildSearchRequest(input.changeSet, contexts, pack, input.checks, input.profile, input.includeCounterReasonInPrompt);
|
|
301
|
+
return {
|
|
302
|
+
contexts,
|
|
303
|
+
pack,
|
|
304
|
+
request,
|
|
305
|
+
estimatedInputTokens: estimatePromptTokens(request.prompt),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async function makeSearchBatchWithPack(input, contexts) {
|
|
309
|
+
const pack = input.profile?.context === "sem"
|
|
310
|
+
? emptyContextPack()
|
|
311
|
+
: await repomixContextPack(input.changeSet.contextCwd, contexts, input.changeSet.changes, input.baseRepomixConfig);
|
|
312
|
+
return makeSearchBatch(input, contexts, pack);
|
|
313
|
+
}
|
|
227
314
|
function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includeCounterReasonInPrompt) {
|
|
228
315
|
return searchRequest({
|
|
229
316
|
changeSet,
|
|
@@ -261,7 +348,7 @@ function sourceLabel(command) {
|
|
|
261
348
|
return "stdin diff";
|
|
262
349
|
}
|
|
263
350
|
function estimatePromptTokens(prompt) {
|
|
264
|
-
return Math.ceil(prompt.length /
|
|
351
|
+
return Math.ceil(prompt.length / 3);
|
|
265
352
|
}
|
|
266
353
|
function countTargetsByPattern(contexts) {
|
|
267
354
|
const counts = {};
|
package/dist/types.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export type StupifyCheck = Readonly<{
|
|
|
68
68
|
id: CheckId;
|
|
69
69
|
name: string;
|
|
70
70
|
question: string;
|
|
71
|
+
why: string;
|
|
71
72
|
lookFor: readonly string[];
|
|
72
73
|
ignoreWhen: readonly string[];
|
|
73
74
|
enabledByDefault?: boolean;
|
|
@@ -194,6 +195,7 @@ export type SearchProfile = Readonly<{
|
|
|
194
195
|
export type SearchMatch = Readonly<{
|
|
195
196
|
targetId: string;
|
|
196
197
|
patternId: CheckId;
|
|
198
|
+
checkWhy?: string;
|
|
197
199
|
reason: string;
|
|
198
200
|
proof: string;
|
|
199
201
|
}>;
|
|
@@ -219,6 +221,8 @@ export type SearchRunJson = Readonly<{
|
|
|
219
221
|
repomixTokens?: number;
|
|
220
222
|
repomixConfig?: RepomixSearchConfig;
|
|
221
223
|
searchTargets?: number;
|
|
224
|
+
searchBatches?: number;
|
|
225
|
+
skippedTargets?: number;
|
|
222
226
|
profileId?: string;
|
|
223
227
|
targetsByPattern?: Readonly<Record<string, number>>;
|
|
224
228
|
targetsPreview?: readonly SearchTargetPreview[];
|
package/package.json
CHANGED
package/src/analysis.ts
CHANGED
|
@@ -104,11 +104,16 @@ function uncheckedSearchMatches(value: unknown, contexts: readonly SemContext[])
|
|
|
104
104
|
targetId,
|
|
105
105
|
patternId: context.checkId,
|
|
106
106
|
reason: match.reason ?? "",
|
|
107
|
-
proof:
|
|
107
|
+
proof: sourcePointer(context),
|
|
108
108
|
}];
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function sourcePointer(context: SemContext): string {
|
|
113
|
+
const file = context.filePath ?? "(unknown)";
|
|
114
|
+
return `${file}::${context.entityKind || "entity"}::${context.entityName || context.entityId}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
112
117
|
async function runJsonPrompt(
|
|
113
118
|
model: LocalModel,
|
|
114
119
|
prompt: string,
|
package/src/checks.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const defaultChecks: readonly StupifyCheck[] = [
|
|
|
5
5
|
id: checkId("duplicated_schema"),
|
|
6
6
|
name: "Duplicated schema",
|
|
7
7
|
question: "Did the change duplicate an existing type, schema, payload, or DTO shape?",
|
|
8
|
+
why: "Duplicated shapes make it easier for AI-assisted changes to drift away from the real source of truth.",
|
|
8
9
|
lookFor: [
|
|
9
10
|
"local shape mirrors existing fields and maps them one-for-one",
|
|
10
11
|
"new response, payload, schema, or DTO adds no filtering, renaming, validation, or versioning",
|
|
@@ -29,6 +30,7 @@ export const defaultChecks: readonly StupifyCheck[] = [
|
|
|
29
30
|
id: checkId("unnecessary_complexity"),
|
|
30
31
|
name: "Unnecessary complexity",
|
|
31
32
|
question: "Did the change add structure without buying clarity?",
|
|
33
|
+
why: "Extra indirection can hide simple decisions and make the code feel more designed than understood.",
|
|
32
34
|
lookFor: [
|
|
33
35
|
"helper, wrapper, service, layer, or extra file around simple logic without reuse",
|
|
34
36
|
],
|
|
@@ -69,6 +71,7 @@ Prefer no match over a weak match.`,
|
|
|
69
71
|
id: checkId("fake_precision_windowing"),
|
|
70
72
|
name: "Fake precision windowing",
|
|
71
73
|
question: "Did the change add fake precision around model context?",
|
|
74
|
+
why: "Precise-looking bookkeeping can create confidence without improving the actual behavior.",
|
|
72
75
|
lookFor: [
|
|
73
76
|
"precise-looking counts, budgets, ratios, reports, or batching fields without useful behavior",
|
|
74
77
|
],
|
|
@@ -81,6 +84,7 @@ Prefer no match over a weak match.`,
|
|
|
81
84
|
id: checkId("coauthored_slop"),
|
|
82
85
|
name: "Coauthored slop",
|
|
83
86
|
question: "Does author metadata contain co-author text?",
|
|
87
|
+
why: "Careless metadata is a cheap signal that the change may not have been reviewed with intent.",
|
|
84
88
|
lookFor: [
|
|
85
89
|
"author signal contains coauhtoried, coauthored, or co-authored text",
|
|
86
90
|
],
|
|
@@ -92,6 +96,7 @@ Prefer no match over a weak match.`,
|
|
|
92
96
|
id: checkId("mega_file"),
|
|
93
97
|
name: "Mega file",
|
|
94
98
|
question: "Is a touched non-config file over 1000 LOC?",
|
|
99
|
+
why: "Large files make judgment harder by concentrating unrelated decisions in one place.",
|
|
95
100
|
lookFor: [
|
|
96
101
|
"touched non-config source file over 1000 LOC",
|
|
97
102
|
],
|
|
@@ -103,6 +108,7 @@ Prefer no match over a weak match.`,
|
|
|
103
108
|
id: checkId("over_commenting"),
|
|
104
109
|
name: "Over commenting",
|
|
105
110
|
question: "Did the change add noisy comments?",
|
|
111
|
+
why: "Narrative comments can make routine code look deliberate without clarifying the underlying tradeoff.",
|
|
106
112
|
lookFor: [
|
|
107
113
|
"comments restate obvious code or narrate simple logic",
|
|
108
114
|
],
|
|
@@ -130,6 +136,7 @@ Prefer no match over a weak match.`,
|
|
|
130
136
|
id: checkId("lint_bypass"),
|
|
131
137
|
name: "Lint bypass",
|
|
132
138
|
question: "Did the change bypass lint or type rules?",
|
|
139
|
+
why: "Unexplained suppressions remove useful feedback exactly where a change needs more scrutiny.",
|
|
133
140
|
lookFor: [
|
|
134
141
|
"adds suppressions, any, broad casts, or weakens lint/typecheck config",
|
|
135
142
|
],
|
|
@@ -153,6 +160,7 @@ Prefer no match over a weak match.`,
|
|
|
153
160
|
id: checkId("inconsistent_patterns"),
|
|
154
161
|
name: "Inconsistent patterns",
|
|
155
162
|
question: "Does the change clash with nearby patterns?",
|
|
163
|
+
why: "Pattern drift can signal that a change followed generic suggestions instead of local codebase judgment.",
|
|
156
164
|
lookFor: [
|
|
157
165
|
"same job uses different naming, errors, state, imports, or layout than nearby files",
|
|
158
166
|
],
|
|
@@ -165,6 +173,7 @@ Prefer no match over a weak match.`,
|
|
|
165
173
|
id: checkId("reinvented_utils"),
|
|
166
174
|
name: "Reinvented utils",
|
|
167
175
|
question: "Did the change recreate an existing utility?",
|
|
176
|
+
why: "Generic helper reinvention can be a sign that the change optimized for plausible code over local reuse.",
|
|
168
177
|
lookFor: [
|
|
169
178
|
"new helper duplicates local utility or standard library behavior",
|
|
170
179
|
],
|
|
@@ -174,7 +183,7 @@ Prefer no match over a weak match.`,
|
|
|
174
183
|
"helper is domain-specific or used by multiple local call sites",
|
|
175
184
|
],
|
|
176
185
|
hookMode: "warn",
|
|
177
|
-
searchPrompt: "Find only tiny generic utility functions that recreate common helpers such as clamp, debounce, throttle, slugify,
|
|
186
|
+
searchPrompt: "Find only tiny generic utility functions that recreate common helpers such as clamp, debounce, throttle, slugify, sort, pick, omit, uniq, or shuffle without domain-specific behavior. Do not match group/resolve/parse/format helpers, domain formatting, feature constants, or helpers with multiple obvious call sites.",
|
|
178
187
|
searchExamples: {
|
|
179
188
|
match: [
|
|
180
189
|
"clampValue returns min, max, or value.",
|
|
@@ -189,6 +198,7 @@ Prefer no match over a weak match.`,
|
|
|
189
198
|
id: checkId("operator_style_mismatch"),
|
|
190
199
|
name: "Operator style mismatch",
|
|
191
200
|
question: "Does the change read unlike the surrounding code?",
|
|
201
|
+
why: "Style mismatch can reveal generic generated code that was not reconciled with nearby conventions.",
|
|
192
202
|
lookFor: [
|
|
193
203
|
"generic or template-like names, abstractions, comments, or control flow clash with local style",
|
|
194
204
|
],
|
package/src/constants.ts
CHANGED
package/src/counter-scout.ts
CHANGED
|
@@ -140,7 +140,7 @@ function lintBypassSignal(value: string): boolean {
|
|
|
140
140
|
|
|
141
141
|
function reinventedUtilitySignal(change: SemChange): boolean {
|
|
142
142
|
const name = change.entityName;
|
|
143
|
-
if (!/^(clamp|debounce|throttle|slug|slugify|
|
|
143
|
+
if (!/^(clamp|debounce|throttle|slug|slugify|sort|shuffle|memoize|pick|omit|uniq)/i.test(name)) return false;
|
|
144
144
|
const content = change.afterContent ?? "";
|
|
145
145
|
if (/currency|invoice|refund|subscription|tier|domain/i.test(`${name}\n${content}`)) return false;
|
|
146
146
|
return true;
|
package/src/prompts.ts
CHANGED
|
@@ -64,6 +64,7 @@ ${input.pack.text || "(none)"}`;
|
|
|
64
64
|
|
|
65
65
|
function formatSearchPattern(check: StupifyCheck): string {
|
|
66
66
|
return `Pattern: ${check.id} (${check.name})
|
|
67
|
+
Why this matters: ${check.why}
|
|
67
68
|
Question: ${check.searchPrompt ?? check.question}
|
|
68
69
|
Look for:
|
|
69
70
|
${check.lookFor.map((signal) => `- ${signal}`).join("\n")}
|
|
@@ -92,6 +93,7 @@ function patternForContext(context: SemContext, patterns: readonly StupifyCheck[
|
|
|
92
93
|
id: context.checkId,
|
|
93
94
|
name: context.checkId,
|
|
94
95
|
question: `Does this target match ${context.checkId}?`,
|
|
96
|
+
why: "This pattern may indicate judgment-offload.",
|
|
95
97
|
lookFor: [],
|
|
96
98
|
ignoreWhen: [],
|
|
97
99
|
};
|
package/src/render.ts
CHANGED
|
@@ -34,7 +34,8 @@ No judgment-offload signals found.`;
|
|
|
34
34
|
return `🧙 stupify 🪄
|
|
35
35
|
Possible judgment-offload detected:
|
|
36
36
|
${run.matches.map((match, index) => `${index + 1}. ${match.patternId}
|
|
37
|
-
${match.
|
|
37
|
+
Why: ${match.checkWhy ?? "This pattern may indicate judgment-offload."}
|
|
38
|
+
Match: ${match.reason}
|
|
38
39
|
Proof: ${match.proof}`).join("\n")}
|
|
39
40
|
Search mode is warn-only.`;
|
|
40
41
|
}
|
package/src/stupify.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { countPromptTokens, runSearch, searchRequest } from "./analysis.ts";
|
|
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
8
|
import { counterScoutTargets } from "./counter-scout.ts";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./search-profile.ts";
|
|
21
21
|
import { semChangeSetForCommand } from "./sem-provider.ts";
|
|
22
22
|
import { createTracer } from "./trace.ts";
|
|
23
|
-
import type { SearchCommand, SearchProfile, SearchRunJson, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
|
|
23
|
+
import type { SearchCommand, SearchMatch, SearchProfile, SearchRunJson, SemContext, SemContextPack, StupifyCheck } from "./types.ts";
|
|
24
24
|
|
|
25
25
|
export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
26
26
|
const startedAt = Date.now();
|
|
@@ -153,17 +153,19 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
153
153
|
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
154
154
|
? initialPack
|
|
155
155
|
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const batches = await buildSearchBatches({
|
|
157
|
+
command,
|
|
158
158
|
changeSet,
|
|
159
|
-
searchContexts,
|
|
160
|
-
pack,
|
|
159
|
+
contexts: searchContexts,
|
|
160
|
+
initialPack: pack,
|
|
161
161
|
checks,
|
|
162
162
|
profile,
|
|
163
|
-
command.includeCounterReasonInPrompt,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
includeCounterReasonInPrompt: command.includeCounterReasonInPrompt,
|
|
164
|
+
maxSearchInputTokens,
|
|
165
|
+
baseRepomixConfig,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (batches.batches.length === 0) {
|
|
167
169
|
return {
|
|
168
170
|
schemaVersion: "search.v1",
|
|
169
171
|
mode: "search",
|
|
@@ -173,7 +175,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
173
175
|
stats: {
|
|
174
176
|
elapsedMs: Date.now() - startedAt,
|
|
175
177
|
modelCalls: 0,
|
|
176
|
-
inputTokens: estimatedInputTokens,
|
|
178
|
+
inputTokens: batches.estimatedInputTokens,
|
|
177
179
|
inputTokenCap: maxSearchInputTokens,
|
|
178
180
|
skipped: true,
|
|
179
181
|
skipReason: "input_too_large",
|
|
@@ -184,6 +186,8 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
184
186
|
repomixFiles: pack.filePaths.length,
|
|
185
187
|
repomixTokens: pack.totalTokens,
|
|
186
188
|
repomixConfig: pack.config,
|
|
189
|
+
searchBatches: 0,
|
|
190
|
+
skippedTargets: batches.skippedTargets,
|
|
187
191
|
profileId: profile?.id,
|
|
188
192
|
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
189
193
|
targetsPreview: previewTargets(searchContexts),
|
|
@@ -192,43 +196,38 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
192
196
|
};
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
if (batches.wasSplit && !command.json) {
|
|
200
|
+
console.error(`Search input is large; queued ${batches.batches.length} smaller search batches.`);
|
|
201
|
+
if (batches.skippedTargets > 0) {
|
|
202
|
+
console.error(`Skipped ${batches.skippedTargets} oversized targets that could not fit alone.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
195
206
|
const modelPath = await firstRunModelBootstrap(command.model);
|
|
196
207
|
const model = await loadLocalModel(modelPath, command.model, "scout");
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
repomixConfig: pack.config,
|
|
219
|
-
profileId: profile?.id,
|
|
220
|
-
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
221
|
-
targetsPreview: previewTargets(searchContexts),
|
|
222
|
-
},
|
|
223
|
-
matches: [],
|
|
224
|
-
};
|
|
208
|
+
const matches = [];
|
|
209
|
+
let modelCalls = 0;
|
|
210
|
+
let inputTokens = 0;
|
|
211
|
+
let exactSkippedTargets = batches.skippedTargets;
|
|
212
|
+
for (const batch of batches.batches) {
|
|
213
|
+
const batchInputTokens = await countPromptTokens(model, batch.request.prompt);
|
|
214
|
+
inputTokens += batchInputTokens;
|
|
215
|
+
if (batchInputTokens > maxSearchInputTokens) {
|
|
216
|
+
exactSkippedTargets += batch.contexts.length;
|
|
217
|
+
if (!command.json) {
|
|
218
|
+
console.error(`Skipped ${batch.contexts.length} targets after exact token count exceeded the limit.`);
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const { value } = await t.trace(
|
|
223
|
+
"search.model",
|
|
224
|
+
() => runSearch(model, batch.request),
|
|
225
|
+
{ count: (v) => v.length },
|
|
226
|
+
);
|
|
227
|
+
modelCalls += 1;
|
|
228
|
+
matches.push(...withCheckWhy(value, checks));
|
|
225
229
|
}
|
|
226
|
-
|
|
227
|
-
const { value: matches } = await t.trace(
|
|
228
|
-
"search.model",
|
|
229
|
-
() => runSearch(model, request),
|
|
230
|
-
{ count: (v) => v.length },
|
|
231
|
-
);
|
|
230
|
+
const uniqueMatches = dedupeMatches(matches);
|
|
232
231
|
|
|
233
232
|
return {
|
|
234
233
|
schemaVersion: "search.v1",
|
|
@@ -238,7 +237,7 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
238
237
|
patterns: patternIds,
|
|
239
238
|
stats: {
|
|
240
239
|
elapsedMs: Date.now() - startedAt,
|
|
241
|
-
modelCalls
|
|
240
|
+
modelCalls,
|
|
242
241
|
inputTokens,
|
|
243
242
|
inputTokenCap: maxSearchInputTokens,
|
|
244
243
|
filesChanged: changeSet.summary.fileCount,
|
|
@@ -248,17 +247,154 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
248
247
|
repomixFiles: pack.filePaths.length,
|
|
249
248
|
repomixTokens: pack.totalTokens,
|
|
250
249
|
repomixConfig: pack.config,
|
|
250
|
+
searchBatches: batches.batches.length,
|
|
251
|
+
skippedTargets: exactSkippedTargets,
|
|
251
252
|
profileId: profile?.id,
|
|
252
253
|
targetsByPattern: countTargetsByPattern(searchContexts),
|
|
253
254
|
targetsPreview: previewTargets(searchContexts),
|
|
254
255
|
},
|
|
255
|
-
matches,
|
|
256
|
+
matches: uniqueMatches,
|
|
256
257
|
};
|
|
257
258
|
} finally {
|
|
258
259
|
await changeSet.cleanup();
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
function dedupeMatches<T extends { targetId: string; patternId: string; proof: string }>(matches: readonly T[]): readonly T[] {
|
|
264
|
+
const seen = new Set<string>();
|
|
265
|
+
return matches.filter((match) => {
|
|
266
|
+
const key = `${match.patternId}\n${match.proof.trim()}`;
|
|
267
|
+
if (seen.has(key)) return false;
|
|
268
|
+
seen.add(key);
|
|
269
|
+
return true;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function withCheckWhy(matches: readonly SearchMatch[], checks: readonly StupifyCheck[]): readonly SearchMatch[] {
|
|
274
|
+
const checksById = new Map(checks.map((check) => [check.id, check]));
|
|
275
|
+
return matches.map((match) => ({
|
|
276
|
+
...match,
|
|
277
|
+
checkWhy: checksById.get(match.patternId)?.why,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
type SearchBatch = Readonly<{
|
|
282
|
+
contexts: readonly SemContext[];
|
|
283
|
+
pack: SemContextPack;
|
|
284
|
+
request: SearchRequest;
|
|
285
|
+
estimatedInputTokens: number;
|
|
286
|
+
}>;
|
|
287
|
+
|
|
288
|
+
async function buildSearchBatches(input: Readonly<{
|
|
289
|
+
command: SearchCommand;
|
|
290
|
+
changeSet: Parameters<typeof searchRequest>[0]["changeSet"];
|
|
291
|
+
contexts: readonly SemContext[];
|
|
292
|
+
initialPack: SemContextPack;
|
|
293
|
+
checks: readonly StupifyCheck[];
|
|
294
|
+
profile: SearchProfile | null;
|
|
295
|
+
includeCounterReasonInPrompt: boolean;
|
|
296
|
+
maxSearchInputTokens: number;
|
|
297
|
+
baseRepomixConfig: Parameters<typeof repomixContextPack>[3];
|
|
298
|
+
}>): Promise<Readonly<{
|
|
299
|
+
batches: readonly SearchBatch[];
|
|
300
|
+
estimatedInputTokens: number;
|
|
301
|
+
skippedTargets: number;
|
|
302
|
+
wasSplit: boolean;
|
|
303
|
+
}>> {
|
|
304
|
+
const first = makeSearchBatch(input, input.contexts, input.initialPack);
|
|
305
|
+
if (first.estimatedInputTokens <= input.maxSearchInputTokens) {
|
|
306
|
+
return {
|
|
307
|
+
batches: [first],
|
|
308
|
+
estimatedInputTokens: first.estimatedInputTokens,
|
|
309
|
+
skippedTargets: 0,
|
|
310
|
+
wasSplit: false,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const batches: SearchBatch[] = [];
|
|
315
|
+
let skippedTargets = 0;
|
|
316
|
+
let currentContexts: readonly SemContext[] = [];
|
|
317
|
+
let currentBatch: SearchBatch | null = null;
|
|
318
|
+
|
|
319
|
+
for (const context of input.contexts) {
|
|
320
|
+
const candidateContexts = [...currentContexts, context];
|
|
321
|
+
const candidateBatch = await makeSearchBatchWithPack(input, candidateContexts);
|
|
322
|
+
if (candidateBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
|
|
323
|
+
currentContexts = candidateContexts;
|
|
324
|
+
currentBatch = candidateBatch;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (currentBatch) {
|
|
329
|
+
batches.push(currentBatch);
|
|
330
|
+
currentContexts = [];
|
|
331
|
+
currentBatch = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const singleBatch = candidateContexts.length === 1
|
|
335
|
+
? candidateBatch
|
|
336
|
+
: await makeSearchBatchWithPack(input, [context]);
|
|
337
|
+
if (singleBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
|
|
338
|
+
currentContexts = [context];
|
|
339
|
+
currentBatch = singleBatch;
|
|
340
|
+
} else {
|
|
341
|
+
skippedTargets += 1;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (currentBatch) batches.push(currentBatch);
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
batches,
|
|
349
|
+
estimatedInputTokens: first.estimatedInputTokens,
|
|
350
|
+
skippedTargets,
|
|
351
|
+
wasSplit: true,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function makeSearchBatch(
|
|
356
|
+
input: Readonly<{
|
|
357
|
+
changeSet: Parameters<typeof searchRequest>[0]["changeSet"];
|
|
358
|
+
checks: readonly StupifyCheck[];
|
|
359
|
+
profile: SearchProfile | null;
|
|
360
|
+
includeCounterReasonInPrompt: boolean;
|
|
361
|
+
}>,
|
|
362
|
+
contexts: readonly SemContext[],
|
|
363
|
+
pack: SemContextPack,
|
|
364
|
+
): SearchBatch {
|
|
365
|
+
const request = buildSearchRequest(
|
|
366
|
+
input.changeSet,
|
|
367
|
+
contexts,
|
|
368
|
+
pack,
|
|
369
|
+
input.checks,
|
|
370
|
+
input.profile,
|
|
371
|
+
input.includeCounterReasonInPrompt,
|
|
372
|
+
);
|
|
373
|
+
return {
|
|
374
|
+
contexts,
|
|
375
|
+
pack,
|
|
376
|
+
request,
|
|
377
|
+
estimatedInputTokens: estimatePromptTokens(request.prompt),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function makeSearchBatchWithPack(
|
|
382
|
+
input: Readonly<{
|
|
383
|
+
command: SearchCommand;
|
|
384
|
+
changeSet: Parameters<typeof searchRequest>[0]["changeSet"];
|
|
385
|
+
checks: readonly StupifyCheck[];
|
|
386
|
+
profile: SearchProfile | null;
|
|
387
|
+
includeCounterReasonInPrompt: boolean;
|
|
388
|
+
baseRepomixConfig: Parameters<typeof repomixContextPack>[3];
|
|
389
|
+
}>,
|
|
390
|
+
contexts: readonly SemContext[],
|
|
391
|
+
): Promise<SearchBatch> {
|
|
392
|
+
const pack = input.profile?.context === "sem"
|
|
393
|
+
? emptyContextPack()
|
|
394
|
+
: await repomixContextPack(input.changeSet.contextCwd, contexts, input.changeSet.changes, input.baseRepomixConfig);
|
|
395
|
+
return makeSearchBatch(input, contexts, pack);
|
|
396
|
+
}
|
|
397
|
+
|
|
262
398
|
function buildSearchRequest(
|
|
263
399
|
changeSet: Parameters<typeof searchRequest>[0]["changeSet"],
|
|
264
400
|
contexts: Parameters<typeof searchRequest>[0]["contexts"],
|
|
@@ -302,7 +438,7 @@ function sourceLabel(command: SearchCommand): string {
|
|
|
302
438
|
}
|
|
303
439
|
|
|
304
440
|
function estimatePromptTokens(prompt: string): number {
|
|
305
|
-
return Math.ceil(prompt.length /
|
|
441
|
+
return Math.ceil(prompt.length / 3);
|
|
306
442
|
}
|
|
307
443
|
|
|
308
444
|
function countTargetsByPattern(contexts: readonly SemContext[]): Record<string, number> {
|
package/src/types.ts
CHANGED
|
@@ -51,6 +51,7 @@ export type StupifyCheck = Readonly<{
|
|
|
51
51
|
id: CheckId;
|
|
52
52
|
name: string;
|
|
53
53
|
question: string;
|
|
54
|
+
why: string;
|
|
54
55
|
lookFor: readonly string[];
|
|
55
56
|
ignoreWhen: readonly string[];
|
|
56
57
|
enabledByDefault?: boolean;
|
|
@@ -192,6 +193,7 @@ export type SearchProfile = Readonly<{
|
|
|
192
193
|
export type SearchMatch = Readonly<{
|
|
193
194
|
targetId: string;
|
|
194
195
|
patternId: CheckId;
|
|
196
|
+
checkWhy?: string;
|
|
195
197
|
reason: string;
|
|
196
198
|
proof: string;
|
|
197
199
|
}>;
|
|
@@ -216,6 +218,8 @@ export type SearchRunJson = Readonly<{
|
|
|
216
218
|
repomixTokens?: number;
|
|
217
219
|
repomixConfig?: RepomixSearchConfig;
|
|
218
220
|
searchTargets?: number;
|
|
221
|
+
searchBatches?: number;
|
|
222
|
+
skippedTargets?: number;
|
|
219
223
|
profileId?: string;
|
|
220
224
|
targetsByPattern?: Readonly<Record<string, number>>;
|
|
221
225
|
targetsPreview?: readonly SearchTargetPreview[];
|