@stupify/cli 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +26 -31
  2. package/dist/analysis.d.ts +11 -9
  3. package/dist/analysis.js +30 -173
  4. package/dist/checks.d.ts +1 -0
  5. package/dist/checks.js +89 -2
  6. package/dist/command.js +55 -91
  7. package/dist/constants.d.ts +1 -1
  8. package/dist/constants.js +1 -1
  9. package/dist/counter-scout.js +70 -8
  10. package/dist/doctor.d.ts +4 -0
  11. package/dist/doctor.js +131 -0
  12. package/dist/git.d.ts +4 -1
  13. package/dist/git.js +34 -0
  14. package/dist/hooks.d.ts +3 -0
  15. package/dist/hooks.js +117 -0
  16. package/dist/model.d.ts +1 -15
  17. package/dist/model.js +37 -21
  18. package/dist/prompts.d.ts +8 -5
  19. package/dist/prompts.js +58 -168
  20. package/dist/render.d.ts +2 -2
  21. package/dist/render.js +70 -78
  22. package/dist/repomix-provider.d.ts +10 -2
  23. package/dist/repomix-provider.js +62 -11
  24. package/dist/search-bench.d.ts +1 -0
  25. package/dist/search-bench.js +675 -0
  26. package/dist/search-profile.d.ts +6 -0
  27. package/dist/search-profile.js +73 -0
  28. package/dist/sem-provider.d.ts +2 -2
  29. package/dist/sem-provider.js +33 -7
  30. package/dist/stupify.d.ts +2 -0
  31. package/dist/stupify.js +185 -334
  32. package/dist/types.d.ts +193 -109
  33. package/package.json +1 -1
  34. package/src/analysis.ts +48 -268
  35. package/src/checks.ts +91 -2
  36. package/src/command.ts +62 -107
  37. package/src/constants.ts +1 -1
  38. package/src/counter-scout.ts +63 -7
  39. package/src/doctor.ts +140 -0
  40. package/src/git.ts +35 -1
  41. package/src/hooks.ts +134 -0
  42. package/src/model.ts +39 -26
  43. package/src/prompts.ts +66 -202
  44. package/src/render.ts +68 -79
  45. package/src/repomix-provider.ts +66 -10
  46. package/src/search-bench.ts +783 -0
  47. package/src/search-profile.ts +89 -0
  48. package/src/sem-provider.ts +36 -9
  49. package/src/stupify.ts +215 -527
  50. package/src/types.ts +195 -119
  51. package/dist/batcher.d.ts +0 -3
  52. package/dist/batcher.js +0 -142
  53. package/dist/candidate-context.d.ts +0 -2
  54. package/dist/candidate-context.js +0 -40
  55. package/dist/experiment.d.ts +0 -1
  56. package/dist/experiment.js +0 -225
  57. package/src/batcher.ts +0 -198
  58. package/src/candidate-context.ts +0 -43
  59. package/src/experiment.ts +0 -317
package/dist/stupify.js CHANGED
@@ -1,21 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
2
3
  import { fileURLToPath } from "node:url";
3
- import { auditCandidates, countPromptTokens, findingsAuditRequest, runFindingsAudit, scoutBatch, scoutSemChanges, } from "./analysis.js";
4
- import { batchDiff } from "./batcher.js";
5
- import { candidateContexts } from "./candidate-context.js";
6
- import { enabledChecks } from "./checks.js";
4
+ import { countPromptTokens, runSearch, searchRequest } from "./analysis.js";
5
+ import { searchChecks } from "./checks.js";
7
6
  import { parseCommand } from "./command.js";
8
- import { MODEL_REGISTRY } from "./constants.js";
9
7
  import { counterScoutTargets } from "./counter-scout.js";
10
- import { readDiffFromStdin } from "./diff.js";
11
- import { runExperiment } from "./experiment.js";
12
- import { netDiffForCommit, netDiffForRecentCommits, netDiffFromStdin, netDiffSince, } from "./git.js";
13
- import { loadLocalModels } from "./model.js";
14
- import { emptyContextPack, entityContextsFromChanges, repomixContextPack } from "./repomix-provider.js";
15
- import { helpText, renderReport } from "./render.js";
8
+ import { runDoctor } from "./doctor.js";
9
+ import { runHookCommand } from "./hooks.js";
10
+ import { firstRunModelBootstrap, loadLocalModel } from "./model.js";
11
+ import { entityContextsFromChanges, emptyContextPack, repomixContextPack, repomixSearchConfig } from "./repomix-provider.js";
12
+ import { helpText, renderSearchRun } from "./render.js";
13
+ import { effectiveMaxCandidates, effectiveMaxSearchInputTokens, effectiveRepomixConfig, effectiveSearchChecks, loadSearchProfile, } from "./search-profile.js";
16
14
  import { semChangeSetForCommand } from "./sem-provider.js";
17
- import { createTracer, trace } from "./trace.js";
18
- const SEM_SCOUT_CHUNK_SIZE = 200;
15
+ import { createTracer } from "./trace.js";
19
16
  export async function main(argv = process.argv.slice(2)) {
20
17
  const startedAt = Date.now();
21
18
  try {
@@ -24,16 +21,22 @@ export async function main(argv = process.argv.slice(2)) {
24
21
  console.log(helpText());
25
22
  return 0;
26
23
  }
27
- if (command.kind === "experiment") {
28
- const outputDir = await runExperiment(command.configPath);
29
- console.log(`Experiment results written to ${outputDir}`);
24
+ if (command.kind === "hook") {
25
+ console.log(await runHookCommand(command.action));
30
26
  return 0;
31
27
  }
32
- const checks = enabledChecks(command.checkIds);
33
- const report = command.engine === "sem"
34
- ? await runSemEngine(command, checks, startedAt)
35
- : await runRawDiffEngine(command, checks, startedAt);
36
- console.log(renderReport(report, command));
28
+ if (command.kind === "doctor") {
29
+ const result = await runDoctor();
30
+ console.log(result.text);
31
+ return result.exitCode;
32
+ }
33
+ if (command.kind === "bench-search") {
34
+ const { runSearchBench } = await import("./search-bench.js");
35
+ console.log(await runSearchBench(command.configPath));
36
+ return 0;
37
+ }
38
+ const run = await runSearchCommand(command, startedAt);
39
+ console.log(renderSearchRun(run, command));
37
40
  return 0;
38
41
  }
39
42
  catch (error) {
@@ -41,347 +44,195 @@ export async function main(argv = process.argv.slice(2)) {
41
44
  return 1;
42
45
  }
43
46
  }
44
- async function runRawDiffEngine(command, checks, startedAt) {
45
- const { value: diff, ms: diffMs } = await trace.trace("net.diff", () => netDiffForCommand(command));
46
- printRunPlan(command, diff, checks.map((check) => check.id));
47
- const { value: models, ms: modelMs } = await trace.trace("model.load", () => loadLocalModels(command.model));
48
- const { scoutModel, auditModel } = models;
49
- const batches = batchDiff(diff.text);
50
- const { value: candidatePointers, ms: searchMs } = await trace.trace("search.total", async () => {
51
- const pointers = [];
52
- for (const batch of batches) {
53
- const { value: candidates } = await trace.trace("search.batch", () => scoutBatch(scoutModel, batch, checks, diff.label), { fields: { batch: batch.id } });
54
- pointers.push(...candidates);
55
- }
56
- return pointers;
57
- });
58
- const contexts = candidateContexts(batches, candidatePointers);
59
- const auditedContexts = contexts;
60
- const { value: result, ms: auditMs } = await trace.trace("audit.candidates", () => auditCandidates(auditModel, diff, auditedContexts, checks), { fields: { candidates: auditedContexts.length } });
61
- return {
62
- run: {
63
- mode: command.kind,
64
- engine: command.engine,
65
- auditContext: command.auditContext,
66
- auditPrompt: command.auditPrompt,
67
- modelId: command.model,
68
- checkIds: checks.map((check) => check.id),
69
- sourceId: diff.id,
70
- label: diff.label,
71
- stats: diff.stats,
72
- batchesScanned: batches.length,
73
- candidateCount: new Set(candidatePointers).size,
74
- entitiesScanned: 0,
75
- auditedCandidateCount: auditedContexts.length,
76
- scoutModelCalls: batches.length,
77
- auditModelCalls: auditedContexts.length > 0 ? 1 : 0,
78
- warnings: [],
79
- timingsMs: {
80
- diff: diffMs,
81
- modelLoad: modelMs,
82
- search: searchMs,
83
- audit: auditMs,
84
- total: Date.now() - startedAt,
85
- },
86
- debugTargets: command.debugTargets ? [] : undefined,
87
- },
88
- result,
89
- };
90
- }
91
- async function runSemEngine(command, checks, startedAt) {
92
- const traceEvents = [];
47
+ export async function runSearchCommand(command, startedAt) {
93
48
  const t = createTracer({
49
+ writeLine: () => undefined,
94
50
  onEvent: (event) => {
95
- traceEvents.push(event);
96
- debugSemTrace(command, event);
51
+ const parts = [`trace ${event.name}`, `${event.ms}ms`];
52
+ if (event.count !== undefined)
53
+ parts.push(`count=${event.count}`);
54
+ if (event.detail)
55
+ parts.push(event.detail);
56
+ console.error(parts.join(" "));
97
57
  },
98
58
  });
99
- const { value: changeSet, ms: diffMs } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
59
+ const profile = await loadSearchProfile(command.searchProfilePath);
60
+ const checks = profile ? effectiveSearchChecks(command.checkIds, profile) : searchChecks(command.checkIds);
61
+ const patternIds = checks.map((check) => check.id);
62
+ const maxCandidates = effectiveMaxCandidates(command.maxCandidates, profile);
63
+ const maxSearchInputTokens = effectiveMaxSearchInputTokens(command.maxSearchInputTokens, profile);
64
+ const { value: changeSet } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
100
65
  count: (v) => v.summary.total,
101
66
  detail: (v) => `${v.summary.fileCount} files`,
102
67
  });
103
- printSemRunPlan(command, changeSet, checks.map((check) => check.id));
104
- const { value: models, ms: modelMs } = await t.trace("model.load", () => loadLocalModels(command.model), {
105
- count: () => 2,
106
- detail: () => "scout+audit",
107
- });
108
- const { scoutModel, auditModel } = models;
109
68
  try {
110
- const candidateBatches = chunkSemChangeSet(changeSet);
111
- const { value: candidates, ms: searchMs } = await t.trace("scout.total", async () => candidateBatches.length === 0
112
- ? []
113
- : command.scout === "counter"
114
- ? counterScoutTargets(changeSet, checks, command.maxCandidates)
115
- : scoutSemBatches(scoutModel, candidateBatches, checks, command, traceEvents, t), {
116
- count: (v) => v.length,
117
- detail: () => `${command.scout} scout ${candidateBatches.length} batches`,
118
- });
119
- const { value: contexts, ms: contextMs } = await t.trace("context.select", async () => entityContextsFromChanges(candidates, changeSet.changes), {
120
- fields: { candidates: candidates.length },
121
- count: (v) => v.length,
122
- detail: (v) => `${new Set(v.map((context) => context.filePath).filter(Boolean)).size} files`,
123
- });
124
- const auditBatches = chunkSemContexts(contexts, command.auditBatchSize);
125
- const { value: result, ms: auditMs } = await t.trace("audit.total", () => findingsAuditBatches(auditModel, changeSet, auditBatches, checks, traceEvents, t, command), {
126
- count: (v) => v.findings.length,
127
- detail: (v) => `${auditBatches.length} batches targets=${v.stats.totalTargets} clean=${v.stats.clean} uncertain=${v.stats.uncertain} invalid=${v.stats.invalid}`,
128
- });
129
- return {
130
- run: {
131
- mode: command.kind,
132
- engine: command.engine,
133
- auditContext: command.auditContext,
134
- auditPrompt: command.auditPrompt,
135
- modelId: command.model,
136
- checkIds: checks.map((check) => check.id),
137
- sourceId: changeSet.id,
138
- label: changeSet.label,
69
+ printRunPlan(command, changeSet.summary.fileCount, changeSet.summary.total, patternIds);
70
+ const candidates = counterScoutTargets(changeSet, checks, maxCandidates);
71
+ const contexts = entityContextsFromChanges(candidates, changeSet.changes);
72
+ const targetsByPattern = countTargetsByPattern(contexts);
73
+ const targetsPreview = previewTargets(contexts);
74
+ if (contexts.length === 0) {
75
+ return {
76
+ schemaVersion: "search.v1",
77
+ mode: "search",
78
+ source: command.source,
79
+ model: { id: command.model },
80
+ patterns: patternIds,
139
81
  stats: {
82
+ elapsedMs: Date.now() - startedAt,
83
+ modelCalls: 0,
84
+ skipped: true,
85
+ skipReason: "no_candidates",
140
86
  filesChanged: changeSet.summary.fileCount,
141
- additions: changeSet.summary.added,
142
- deletions: changeSet.summary.deleted,
87
+ entitiesScanned: changeSet.summary.total,
88
+ candidates: 0,
89
+ searchTargets: 0,
90
+ repomixFiles: 0,
91
+ repomixTokens: 0,
92
+ profileId: profile?.id,
93
+ targetsByPattern,
94
+ targetsPreview,
143
95
  },
144
- batchesScanned: 0,
145
- entitiesScanned: changeSet.summary.total,
146
- candidateCount: candidates.length,
147
- targetsByCheck: countTargetsByCheck(candidates),
148
- auditedCandidateCount: contexts.length,
149
- scoutModelCalls: traceEvents.filter((event) => event.name === "scout.batch").length,
150
- auditModelCalls: result.auditModelCalls,
151
- timingsMs: {
152
- diff: diffMs,
153
- modelLoad: modelMs,
154
- search: searchMs,
155
- audit: auditMs + contextMs,
156
- total: Date.now() - startedAt,
96
+ matches: [],
97
+ };
98
+ }
99
+ const baseRepomixConfig = effectiveRepomixConfig(repomixSearchConfig(), profile);
100
+ const initialPack = profile?.context === "sem"
101
+ ? emptyContextPack()
102
+ : await t.trace("context.pack", () => repomixContextPack(changeSet.contextCwd, contexts, changeSet.changes, baseRepomixConfig), {
103
+ count: (v) => v.filePaths.length,
104
+ detail: (v) => `${v.totalTokens} tokens`,
105
+ }).then((result) => result.value);
106
+ const packedFiles = new Set(initialPack.filePaths);
107
+ const searchContexts = profile?.context === "sem"
108
+ ? contexts
109
+ : contexts.filter((context) => context.filePath && packedFiles.has(context.filePath));
110
+ if (searchContexts.length === 0) {
111
+ return {
112
+ schemaVersion: "search.v1",
113
+ mode: "search",
114
+ source: command.source,
115
+ model: { id: command.model },
116
+ patterns: patternIds,
117
+ stats: {
118
+ elapsedMs: Date.now() - startedAt,
119
+ modelCalls: 0,
120
+ skipped: true,
121
+ skipReason: "no_candidates",
122
+ filesChanged: changeSet.summary.fileCount,
123
+ entitiesScanned: changeSet.summary.total,
124
+ candidates: contexts.length,
125
+ searchTargets: 0,
126
+ repomixFiles: initialPack.filePaths.length,
127
+ repomixTokens: initialPack.totalTokens,
128
+ repomixConfig: initialPack.config,
129
+ profileId: profile?.id,
130
+ targetsByPattern,
131
+ targetsPreview,
157
132
  },
158
- warnings: [],
159
- auditStats: result.stats,
160
- debugTargets: command.debugTargets ? debugTargetsFromContexts(contexts, changeSet.label) : undefined,
161
- traceEvents,
133
+ matches: [],
134
+ };
135
+ }
136
+ const pack = profile?.context === "sem" || searchContexts.length === contexts.length
137
+ ? initialPack
138
+ : await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
139
+ const modelPath = await firstRunModelBootstrap(command.model);
140
+ const model = await loadLocalModel(modelPath, command.model, "scout");
141
+ const request = buildSearchRequest(changeSet, searchContexts, pack, checks, profile, command.includeCounterReasonInPrompt);
142
+ const inputTokens = await countPromptTokens(model, request.prompt);
143
+ if (inputTokens > maxSearchInputTokens) {
144
+ return {
145
+ schemaVersion: "search.v1",
146
+ mode: "search",
147
+ source: command.source,
148
+ model: { id: command.model },
149
+ patterns: patternIds,
150
+ stats: {
151
+ elapsedMs: Date.now() - startedAt,
152
+ modelCalls: 0,
153
+ inputTokens,
154
+ inputTokenCap: maxSearchInputTokens,
155
+ skipped: true,
156
+ skipReason: "input_too_large",
157
+ filesChanged: changeSet.summary.fileCount,
158
+ entitiesScanned: changeSet.summary.total,
159
+ candidates: contexts.length,
160
+ searchTargets: searchContexts.length,
161
+ repomixFiles: pack.filePaths.length,
162
+ repomixTokens: pack.totalTokens,
163
+ repomixConfig: pack.config,
164
+ profileId: profile?.id,
165
+ targetsByPattern: countTargetsByPattern(searchContexts),
166
+ targetsPreview: previewTargets(searchContexts),
167
+ },
168
+ matches: [],
169
+ };
170
+ }
171
+ const { value: matches } = await t.trace("search.model", () => runSearch(model, request), { count: (v) => v.length });
172
+ return {
173
+ schemaVersion: "search.v1",
174
+ mode: "search",
175
+ source: command.source,
176
+ model: { id: command.model },
177
+ patterns: patternIds,
178
+ stats: {
179
+ elapsedMs: Date.now() - startedAt,
180
+ modelCalls: 1,
181
+ inputTokens,
182
+ inputTokenCap: maxSearchInputTokens,
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),
162
193
  },
163
- result,
194
+ matches,
164
195
  };
165
196
  }
166
197
  finally {
167
198
  await changeSet.cleanup();
168
199
  }
169
200
  }
170
- async function findingsAuditBatches(model, changeSet, batches, checks, traceEvents, t, command) {
171
- const findings = [];
172
- const stats = { totalTargets: 0, finding: 0, clean: 0, uncertain: 0, invalid: 0 };
173
- const limiter = new ConcurrencyLimiter(command.auditConcurrency);
174
- for (const [index, batch] of batches.entries()) {
175
- const result = await findingsAuditBatch(model, changeSet, batch, checks, traceEvents, command, limiter, `${index + 1}/${batches.length}`);
176
- findings.push(...result.findings);
177
- stats.totalTargets += result.stats.totalTargets;
178
- stats.finding += result.stats.finding;
179
- stats.clean += result.stats.clean;
180
- stats.uncertain += result.stats.uncertain;
181
- stats.invalid += result.stats.invalid;
182
- }
183
- return {
184
- findings,
185
- summary: findings.length === 0
186
- ? "No clear judgment-offload signal found."
187
- : `${findings.length} finding review${findings.length === 1 ? "" : "s"} accepted.`,
188
- stats,
189
- auditModelCalls: traceEvents.filter((event) => event.name === "audit.batch").length,
190
- };
191
- }
192
- async function findingsAuditBatch(model, changeSet, batch, checks, traceEvents, command, limiter, batchLabel) {
193
- const { value: pack, ms: contextMs } = await trace.trace("context.pack", () => command.auditContext === "none"
194
- ? Promise.resolve(emptyContextPack())
195
- : repomixContextPack(changeSet.contextCwd, batch, changeSet.changes), { fields: { candidates: batch.length } });
196
- const request = findingsAuditRequest(changeSet, batch, pack, checks, command.auditPrompt);
197
- const inputTokens = await countPromptTokens(model, request.prompt);
198
- const contextEvent = {
199
- name: "context.pack",
200
- ms: contextMs,
201
- count: pack.filePaths.length,
202
- detail: `batch=${batchLabel} input_tokens=${inputTokens} pack_tokens=${pack.totalTokens} chars=${pack.totalCharacters}`,
203
- };
204
- traceEvents.push(contextEvent);
205
- debugSemTrace(command, contextEvent);
206
- if (inputTokens > command.maxAuditInputTokens) {
207
- if (batch.length <= 1) {
208
- throw new Error(`Findings audit input has ${inputTokens} tokens, above max ${command.maxAuditInputTokens}.`);
209
- }
210
- const splitAt = Math.ceil(batch.length / 2);
211
- const splitEvent = {
212
- name: "audit.split",
213
- ms: 0,
214
- count: batch.length,
215
- detail: `batch=${batchLabel} input_tokens=${inputTokens} max=${command.maxAuditInputTokens}`,
216
- };
217
- traceEvents.push(splitEvent);
218
- debugSemTrace(command, splitEvent);
219
- const [left, right] = await Promise.all([
220
- findingsAuditBatch(model, changeSet, batch.slice(0, splitAt), checks, traceEvents, command, limiter, `${batchLabel}.1`),
221
- findingsAuditBatch(model, changeSet, batch.slice(splitAt), checks, traceEvents, command, limiter, `${batchLabel}.2`),
222
- ]);
223
- return combineAuditResults(left, right);
224
- }
225
- const { value: result, ms: auditMs } = await trace.trace("audit.batch", () => limiter.run(() => runFindingsAudit(model, changeSet, batch, pack, checks, request)), { fields: { candidates: batch.length } });
226
- const event = {
227
- name: "audit.batch",
228
- ms: auditMs,
229
- count: result.findings.length,
230
- detail: `batch=${batchLabel} candidates=${batch.length} input_tokens=${inputTokens} targets=${result.stats.totalTargets} clean=${result.stats.clean} uncertain=${result.stats.uncertain} invalid=${result.stats.invalid}`,
231
- };
232
- traceEvents.push(event);
233
- debugSemTrace(command, event);
234
- return result;
201
+ function buildSearchRequest(changeSet, contexts, pack, patterns, profile, includeCounterReasonInPrompt) {
202
+ return searchRequest({
203
+ changeSet,
204
+ contexts,
205
+ pack,
206
+ patterns,
207
+ includeCounterReasonInPrompt: profile?.includeCounterReasonInPrompt ?? includeCounterReasonInPrompt,
208
+ });
235
209
  }
236
- function combineAuditResults(left, right) {
237
- const findings = [...left.findings, ...right.findings];
238
- return {
239
- findings,
240
- summary: findings.length === 0
241
- ? "No clear judgment-offload signal found."
242
- : `${findings.length} finding review${findings.length === 1 ? "" : "s"} accepted.`,
243
- stats: {
244
- totalTargets: left.stats.totalTargets + right.stats.totalTargets,
245
- finding: left.stats.finding + right.stats.finding,
246
- clean: left.stats.clean + right.stats.clean,
247
- uncertain: left.stats.uncertain + right.stats.uncertain,
248
- invalid: left.stats.invalid + right.stats.invalid,
249
- },
250
- };
210
+ function printRunPlan(command, filesChanged, entitiesScanned, patternIds) {
211
+ if (command.json)
212
+ return;
213
+ console.error("🧙 stupify 🪄");
214
+ console.error(`Mode: search (${command.source})`);
215
+ console.error(`Sem: ${filesChanged} files, ${entitiesScanned} changed entities`);
216
+ console.error(`Patterns: ${patternIds.join(", ")}`);
251
217
  }
252
- function countTargetsByCheck(candidates) {
218
+ function countTargetsByPattern(contexts) {
253
219
  const counts = {};
254
- for (const candidate of candidates) {
255
- counts[candidate.checkId] = (counts[candidate.checkId] ?? 0) + 1;
256
- }
220
+ for (const context of contexts)
221
+ counts[context.checkId] = (counts[context.checkId] ?? 0) + 1;
257
222
  return counts;
258
223
  }
259
- function debugTargetsFromContexts(contexts, sourceLabel) {
224
+ function previewTargets(contexts) {
260
225
  return contexts.map((context) => ({
261
226
  targetId: context.targetId,
262
- checkId: context.checkId,
263
- entityId: context.entityId,
264
- entityKind: context.entityKind,
265
- changeKind: context.changeKind,
266
- scoutReason: context.reason,
267
- sourceLabel,
227
+ patternId: context.checkId,
228
+ entityKind: context.entityKind || undefined,
229
+ sourceKind: context.filePath ? pathKind(context.filePath) : undefined,
268
230
  }));
269
231
  }
270
- class ConcurrencyLimiter {
271
- max;
272
- active = 0;
273
- queue = [];
274
- constructor(max) {
275
- this.max = max;
276
- }
277
- async run(task) {
278
- if (this.active >= this.max) {
279
- await new Promise((resolve) => this.queue.push(resolve));
280
- }
281
- this.active += 1;
282
- try {
283
- return await task();
284
- }
285
- finally {
286
- this.active -= 1;
287
- this.queue.shift()?.();
288
- }
289
- }
290
- }
291
- async function scoutSemBatches(model, batches, checks, command, traceEvents, t) {
292
- const candidates = [];
293
- const seen = new Set();
294
- const targetsByCheck = new Map();
295
- const maxTargetsPerCheck = 6;
296
- for (const [index, batch] of batches.entries()) {
297
- if (candidates.length >= command.maxCandidates)
298
- break;
299
- const remaining = command.maxCandidates - candidates.length;
300
- const { value: batchCandidates } = await t.trace("scout.batch", async () => scoutSemChanges(model, batch, checks, remaining), {
301
- fields: { entities: batch.changes.length },
302
- count: (v) => v.length,
303
- detail: (v) => `batch=${index + 1}/${batches.length} entities=${batch.changes.length} remaining=${remaining}`,
304
- });
305
- for (const candidate of batchCandidates) {
306
- const key = `${candidate.entityId}\u0000${candidate.checkId}`;
307
- if (seen.has(key))
308
- continue;
309
- const checkCount = targetsByCheck.get(candidate.checkId) ?? 0;
310
- if (checkCount >= maxTargetsPerCheck)
311
- continue;
312
- seen.add(key);
313
- targetsByCheck.set(candidate.checkId, checkCount + 1);
314
- candidates.push({
315
- ...candidate,
316
- targetId: `t${String(candidates.length + 1).padStart(3, "0")}`,
317
- });
318
- if (candidates.length >= command.maxCandidates)
319
- break;
320
- }
321
- }
322
- return candidates;
323
- }
324
- function debugSemTrace(command, event) {
325
- if (!command.debugSem)
326
- return;
327
- const parts = [`trace ${event.name}`, `${event.ms}ms`];
328
- if (event.count !== undefined)
329
- parts.push(`count=${event.count}`);
330
- if (event.detail)
331
- parts.push(event.detail);
332
- console.error(parts.join(" "));
333
- }
334
- function chunkSemChangeSet(changeSet) {
335
- const chunks = [];
336
- for (let index = 0; index < changeSet.changes.length; index += SEM_SCOUT_CHUNK_SIZE) {
337
- const changes = changeSet.changes.slice(index, index + SEM_SCOUT_CHUNK_SIZE);
338
- chunks.push({
339
- ...changeSet,
340
- label: `${changeSet.label} batch ${chunks.length + 1}`,
341
- changes,
342
- summary: {
343
- ...changeSet.summary,
344
- fileCount: new Set(changes.map((change) => change.filePath)).size,
345
- total: changes.length,
346
- },
347
- });
348
- }
349
- return chunks;
350
- }
351
- function chunkSemContexts(contexts, chunkSize) {
352
- const chunks = [];
353
- for (let index = 0; index < contexts.length; index += chunkSize) {
354
- chunks.push(contexts.slice(index, index + chunkSize));
355
- }
356
- return chunks;
357
- }
358
- function printRunPlan(command, diff, checkIds) {
359
- if (command.json)
360
- return;
361
- console.error("🧙 stupify 🪄");
362
- console.error(`Window: ${diff.label}`);
363
- console.error(`Diff: ${diff.stats.filesChanged} files changed, ${diff.stats.additions} added, ${diff.stats.deletions} deleted`);
364
- console.error(`Model: ${MODEL_REGISTRY[command.model].name}`);
365
- console.error(`Checks: ${checkIds.join(", ")}`);
366
- }
367
- function printSemRunPlan(command, changeSet, checkIds) {
368
- if (command.json)
369
- return;
370
- console.error("🧙 stupify 🪄");
371
- console.error(`Window: ${changeSet.label}`);
372
- console.error(`Sem: ${changeSet.summary.fileCount} files, ${changeSet.summary.total} changed entities`);
373
- console.error(`Model: ${MODEL_REGISTRY[command.model].name}`);
374
- console.error(`Checks: ${checkIds.join(", ")}`);
375
- }
376
- async function netDiffForCommand(command) {
377
- if (command.kind === "since")
378
- return netDiffSince(command.since);
379
- if (command.kind === "stdin")
380
- return netDiffFromStdin(await readDiffFromStdin());
381
- if (command.kind === "commit")
382
- return netDiffForCommit(command.commit);
383
- return netDiffForRecentCommits(command.count);
232
+ function pathKind(filePath) {
233
+ const ext = filePath.split(".").pop();
234
+ return ext && ext !== filePath ? ext : "unknown";
384
235
  }
385
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
236
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
386
237
  process.exitCode = await main();
387
238
  }