@stupify/cli 0.0.6 → 0.0.7

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/checks.js CHANGED
@@ -173,7 +173,7 @@ Prefer no match over a weak match.`,
173
173
  "helper is domain-specific or used by multiple local call sites",
174
174
  ],
175
175
  hookMode: "warn",
176
- searchPrompt: "Find only tiny generic utility functions that recreate common helpers such as clamp, debounce, throttle, slugify, group, sort, pick, omit, uniq, or shuffle without domain-specific behavior. Do not match resolve/parse/format helpers, domain formatting, feature constants, or helpers with multiple obvious call sites.",
176
+ 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
177
  searchExamples: {
178
178
  match: [
179
179
  "clampValue returns min, max, or value.",
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.0.6";
1
+ export declare const VERSION = "0.0.7";
2
2
  import type { ModelConfig, ModelId } from "./types.ts";
3
3
  export declare const DEFAULT_MODEL_ID: ModelId;
4
4
  export declare const MODEL_REGISTRY: Record<ModelId, ModelConfig>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "0.0.6";
1
+ export const VERSION = "0.0.7";
2
2
  export const DEFAULT_MODEL_ID = "gemma-4-e2b";
3
3
  export const MODEL_REGISTRY = {
4
4
  "gemma-4-e2b": {
@@ -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|group|sort|shuffle|memoize|pick|omit|uniq)/i.test(name))
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/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 request = buildSearchRequest(changeSet, searchContexts, pack, checks, profile, command.includeCounterReasonInPrompt);
137
- const estimatedInputTokens = estimatePromptTokens(request.prompt);
138
- if (estimatedInputTokens > maxSearchInputTokens) {
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 inputTokens = await countPromptTokens(model, request.prompt);
169
- if (inputTokens > maxSearchInputTokens) {
170
- return {
171
- schemaVersion: "search.v1",
172
- mode: "search",
173
- source: command.source,
174
- model: { id: command.model },
175
- patterns: patternIds,
176
- stats: {
177
- elapsedMs: Date.now() - startedAt,
178
- modelCalls: 0,
179
- inputTokens,
180
- inputTokenCap: maxSearchInputTokens,
181
- skipped: true,
182
- skipReason: "input_too_large",
183
- filesChanged: changeSet.summary.fileCount,
184
- entitiesScanned: changeSet.summary.total,
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(...value);
196
202
  }
197
- const { value: matches } = await t.trace("search.model", () => runSearch(model, request), { count: (v) => v.length });
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: 1,
212
+ modelCalls,
207
213
  inputTokens,
208
214
  inputTokenCap: maxSearchInputTokens,
209
215
  filesChanged: changeSet.summary.fileCount,
@@ -213,17 +219,91 @@ 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
+ async function buildSearchBatches(input) {
246
+ const first = makeSearchBatch(input, input.contexts, input.initialPack);
247
+ if (first.estimatedInputTokens <= input.maxSearchInputTokens) {
248
+ return {
249
+ batches: [first],
250
+ estimatedInputTokens: first.estimatedInputTokens,
251
+ skippedTargets: 0,
252
+ wasSplit: false,
253
+ };
254
+ }
255
+ const batches = [];
256
+ let skippedTargets = 0;
257
+ let currentContexts = [];
258
+ let currentBatch = null;
259
+ for (const context of input.contexts) {
260
+ const candidateContexts = [...currentContexts, context];
261
+ const candidateBatch = await makeSearchBatchWithPack(input, candidateContexts);
262
+ if (candidateBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
263
+ currentContexts = candidateContexts;
264
+ currentBatch = candidateBatch;
265
+ continue;
266
+ }
267
+ if (currentBatch) {
268
+ batches.push(currentBatch);
269
+ currentContexts = [];
270
+ currentBatch = null;
271
+ }
272
+ const singleBatch = candidateContexts.length === 1
273
+ ? candidateBatch
274
+ : await makeSearchBatchWithPack(input, [context]);
275
+ if (singleBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
276
+ currentContexts = [context];
277
+ currentBatch = singleBatch;
278
+ }
279
+ else {
280
+ skippedTargets += 1;
281
+ }
282
+ }
283
+ if (currentBatch)
284
+ batches.push(currentBatch);
285
+ return {
286
+ batches,
287
+ estimatedInputTokens: first.estimatedInputTokens,
288
+ skippedTargets,
289
+ wasSplit: true,
290
+ };
291
+ }
292
+ function makeSearchBatch(input, contexts, pack) {
293
+ const request = buildSearchRequest(input.changeSet, contexts, pack, input.checks, input.profile, input.includeCounterReasonInPrompt);
294
+ return {
295
+ contexts,
296
+ pack,
297
+ request,
298
+ estimatedInputTokens: estimatePromptTokens(request.prompt),
299
+ };
300
+ }
301
+ async function makeSearchBatchWithPack(input, contexts) {
302
+ const pack = input.profile?.context === "sem"
303
+ ? emptyContextPack()
304
+ : await repomixContextPack(input.changeSet.contextCwd, contexts, input.changeSet.changes, input.baseRepomixConfig);
305
+ return makeSearchBatch(input, contexts, pack);
306
+ }
227
307
  function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includeCounterReasonInPrompt) {
228
308
  return searchRequest({
229
309
  changeSet,
@@ -261,7 +341,7 @@ function sourceLabel(command) {
261
341
  return "stdin diff";
262
342
  }
263
343
  function estimatePromptTokens(prompt) {
264
- return Math.ceil(prompt.length / 4);
344
+ return Math.ceil(prompt.length / 3);
265
345
  }
266
346
  function countTargetsByPattern(contexts) {
267
347
  const counts = {};
package/dist/types.d.ts CHANGED
@@ -219,6 +219,8 @@ export type SearchRunJson = Readonly<{
219
219
  repomixTokens?: number;
220
220
  repomixConfig?: RepomixSearchConfig;
221
221
  searchTargets?: number;
222
+ searchBatches?: number;
223
+ skippedTargets?: number;
222
224
  profileId?: string;
223
225
  targetsByPattern?: Readonly<Record<string, number>>;
224
226
  targetsPreview?: readonly SearchTargetPreview[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stupify/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Local-only diagnostic CLI for checking whether AI is making you dumber.",
5
5
  "private": false,
6
6
  "type": "module",
package/src/checks.ts CHANGED
@@ -174,7 +174,7 @@ Prefer no match over a weak match.`,
174
174
  "helper is domain-specific or used by multiple local call sites",
175
175
  ],
176
176
  hookMode: "warn",
177
- searchPrompt: "Find only tiny generic utility functions that recreate common helpers such as clamp, debounce, throttle, slugify, group, sort, pick, omit, uniq, or shuffle without domain-specific behavior. Do not match resolve/parse/format helpers, domain formatting, feature constants, or helpers with multiple obvious call sites.",
177
+ 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
178
  searchExamples: {
179
179
  match: [
180
180
  "clampValue returns min, max, or value.",
package/src/constants.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = "0.0.6";
1
+ export const VERSION = "0.0.7";
2
2
  import type { ModelConfig, ModelId } from "./types.ts";
3
3
 
4
4
  export const DEFAULT_MODEL_ID: ModelId = "gemma-4-e2b";
@@ -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|group|sort|shuffle|memoize|pick|omit|uniq)/i.test(name)) return false;
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/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";
@@ -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
- const request = buildSearchRequest(
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
- const estimatedInputTokens = estimatePromptTokens(request.prompt);
166
- if (estimatedInputTokens > maxSearchInputTokens) {
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 inputTokens = await countPromptTokens(model, request.prompt);
198
- if (inputTokens > maxSearchInputTokens) {
199
- return {
200
- schemaVersion: "search.v1",
201
- mode: "search",
202
- source: command.source,
203
- model: { id: command.model },
204
- patterns: patternIds,
205
- stats: {
206
- elapsedMs: Date.now() - startedAt,
207
- modelCalls: 0,
208
- inputTokens,
209
- inputTokenCap: maxSearchInputTokens,
210
- skipped: true,
211
- skipReason: "input_too_large",
212
- filesChanged: changeSet.summary.fileCount,
213
- entitiesScanned: changeSet.summary.total,
214
- candidates: contexts.length,
215
- searchTargets: searchContexts.length,
216
- repomixFiles: pack.filePaths.length,
217
- repomixTokens: pack.totalTokens,
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(...value);
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: 1,
240
+ modelCalls,
242
241
  inputTokens,
243
242
  inputTokenCap: maxSearchInputTokens,
244
243
  filesChanged: changeSet.summary.fileCount,
@@ -248,17 +247,146 @@ 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
+ type SearchBatch = Readonly<{
274
+ contexts: readonly SemContext[];
275
+ pack: SemContextPack;
276
+ request: SearchRequest;
277
+ estimatedInputTokens: number;
278
+ }>;
279
+
280
+ async function buildSearchBatches(input: Readonly<{
281
+ command: SearchCommand;
282
+ changeSet: Parameters<typeof searchRequest>[0]["changeSet"];
283
+ contexts: readonly SemContext[];
284
+ initialPack: SemContextPack;
285
+ checks: readonly StupifyCheck[];
286
+ profile: SearchProfile | null;
287
+ includeCounterReasonInPrompt: boolean;
288
+ maxSearchInputTokens: number;
289
+ baseRepomixConfig: Parameters<typeof repomixContextPack>[3];
290
+ }>): Promise<Readonly<{
291
+ batches: readonly SearchBatch[];
292
+ estimatedInputTokens: number;
293
+ skippedTargets: number;
294
+ wasSplit: boolean;
295
+ }>> {
296
+ const first = makeSearchBatch(input, input.contexts, input.initialPack);
297
+ if (first.estimatedInputTokens <= input.maxSearchInputTokens) {
298
+ return {
299
+ batches: [first],
300
+ estimatedInputTokens: first.estimatedInputTokens,
301
+ skippedTargets: 0,
302
+ wasSplit: false,
303
+ };
304
+ }
305
+
306
+ const batches: SearchBatch[] = [];
307
+ let skippedTargets = 0;
308
+ let currentContexts: readonly SemContext[] = [];
309
+ let currentBatch: SearchBatch | null = null;
310
+
311
+ for (const context of input.contexts) {
312
+ const candidateContexts = [...currentContexts, context];
313
+ const candidateBatch = await makeSearchBatchWithPack(input, candidateContexts);
314
+ if (candidateBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
315
+ currentContexts = candidateContexts;
316
+ currentBatch = candidateBatch;
317
+ continue;
318
+ }
319
+
320
+ if (currentBatch) {
321
+ batches.push(currentBatch);
322
+ currentContexts = [];
323
+ currentBatch = null;
324
+ }
325
+
326
+ const singleBatch = candidateContexts.length === 1
327
+ ? candidateBatch
328
+ : await makeSearchBatchWithPack(input, [context]);
329
+ if (singleBatch.estimatedInputTokens <= input.maxSearchInputTokens) {
330
+ currentContexts = [context];
331
+ currentBatch = singleBatch;
332
+ } else {
333
+ skippedTargets += 1;
334
+ }
335
+ }
336
+
337
+ if (currentBatch) batches.push(currentBatch);
338
+
339
+ return {
340
+ batches,
341
+ estimatedInputTokens: first.estimatedInputTokens,
342
+ skippedTargets,
343
+ wasSplit: true,
344
+ };
345
+ }
346
+
347
+ function makeSearchBatch(
348
+ input: Readonly<{
349
+ changeSet: Parameters<typeof searchRequest>[0]["changeSet"];
350
+ checks: readonly StupifyCheck[];
351
+ profile: SearchProfile | null;
352
+ includeCounterReasonInPrompt: boolean;
353
+ }>,
354
+ contexts: readonly SemContext[],
355
+ pack: SemContextPack,
356
+ ): SearchBatch {
357
+ const request = buildSearchRequest(
358
+ input.changeSet,
359
+ contexts,
360
+ pack,
361
+ input.checks,
362
+ input.profile,
363
+ input.includeCounterReasonInPrompt,
364
+ );
365
+ return {
366
+ contexts,
367
+ pack,
368
+ request,
369
+ estimatedInputTokens: estimatePromptTokens(request.prompt),
370
+ };
371
+ }
372
+
373
+ async function makeSearchBatchWithPack(
374
+ input: Readonly<{
375
+ command: SearchCommand;
376
+ changeSet: Parameters<typeof searchRequest>[0]["changeSet"];
377
+ checks: readonly StupifyCheck[];
378
+ profile: SearchProfile | null;
379
+ includeCounterReasonInPrompt: boolean;
380
+ baseRepomixConfig: Parameters<typeof repomixContextPack>[3];
381
+ }>,
382
+ contexts: readonly SemContext[],
383
+ ): Promise<SearchBatch> {
384
+ const pack = input.profile?.context === "sem"
385
+ ? emptyContextPack()
386
+ : await repomixContextPack(input.changeSet.contextCwd, contexts, input.changeSet.changes, input.baseRepomixConfig);
387
+ return makeSearchBatch(input, contexts, pack);
388
+ }
389
+
262
390
  function buildSearchRequest(
263
391
  changeSet: Parameters<typeof searchRequest>[0]["changeSet"],
264
392
  contexts: Parameters<typeof searchRequest>[0]["contexts"],
@@ -302,7 +430,7 @@ function sourceLabel(command: SearchCommand): string {
302
430
  }
303
431
 
304
432
  function estimatePromptTokens(prompt: string): number {
305
- return Math.ceil(prompt.length / 4);
433
+ return Math.ceil(prompt.length / 3);
306
434
  }
307
435
 
308
436
  function countTargetsByPattern(contexts: readonly SemContext[]): Record<string, number> {
package/src/types.ts CHANGED
@@ -216,6 +216,8 @@ export type SearchRunJson = Readonly<{
216
216
  repomixTokens?: number;
217
217
  repomixConfig?: RepomixSearchConfig;
218
218
  searchTargets?: number;
219
+ searchBatches?: number;
220
+ skippedTargets?: number;
219
221
  profileId?: string;
220
222
  targetsByPattern?: Readonly<Record<string, number>>;
221
223
  targetsPreview?: readonly SearchTargetPreview[];