@stupify/cli 0.0.2 → 0.0.3
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/README.md +60 -0
- package/dist/analysis.d.ts +14 -0
- package/dist/analysis.js +276 -0
- package/dist/batcher.d.ts +3 -0
- package/dist/batcher.js +142 -0
- package/dist/cache.d.ts +2 -0
- package/dist/cache.js +59 -0
- package/dist/candidate-context.d.ts +2 -0
- package/dist/candidate-context.js +40 -0
- package/dist/checks.d.ts +3 -0
- package/dist/checks.js +131 -0
- package/dist/command.d.ts +2 -0
- package/dist/command.js +183 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +53 -0
- package/dist/counter-scout.d.ts +14 -0
- package/dist/counter-scout.js +97 -0
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +10 -0
- package/dist/experiment.d.ts +1 -0
- package/dist/experiment.js +225 -0
- package/dist/git.d.ts +8 -0
- package/dist/git.js +219 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +24 -0
- package/dist/model.js +281 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.js +197 -0
- package/dist/render.d.ts +3 -0
- package/dist/render.js +101 -0
- package/dist/repomix-provider.d.ts +4 -0
- package/dist/repomix-provider.js +145 -0
- package/dist/sem-provider.d.ts +2 -0
- package/dist/sem-provider.js +221 -0
- package/dist/stupify.d.ts +2 -0
- package/dist/stupify.js +387 -0
- package/dist/trace.d.ts +29 -0
- package/dist/trace.js +64 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.js +6 -0
- package/package.json +42 -5
- package/src/analysis.ts +408 -0
- package/src/batcher.ts +198 -0
- package/src/cache.ts +65 -0
- package/src/candidate-context.ts +43 -0
- package/src/checks.ts +132 -0
- package/src/command.ts +218 -0
- package/src/constants.ts +56 -0
- package/src/counter-scout.ts +119 -0
- package/src/diff.ts +9 -0
- package/src/experiment.ts +317 -0
- package/src/git.ts +228 -0
- package/src/index.ts +1 -0
- package/src/model.ts +360 -0
- package/src/prompts.ts +234 -0
- package/src/render.ts +107 -0
- package/src/repomix-provider.ts +163 -0
- package/src/sem-provider.ts +255 -0
- package/src/stupify.ts +598 -0
- package/src/trace.ts +103 -0
- package/src/types.ts +264 -0
- package/bin/stupify.mjs +0 -3
package/dist/stupify.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
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";
|
|
7
|
+
import { parseCommand } from "./command.js";
|
|
8
|
+
import { MODEL_REGISTRY } from "./constants.js";
|
|
9
|
+
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";
|
|
16
|
+
import { semChangeSetForCommand } from "./sem-provider.js";
|
|
17
|
+
import { createTracer, trace } from "./trace.js";
|
|
18
|
+
const SEM_SCOUT_CHUNK_SIZE = 200;
|
|
19
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
try {
|
|
22
|
+
const command = parseCommand(argv);
|
|
23
|
+
if (command.kind === "help") {
|
|
24
|
+
console.log(helpText());
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (command.kind === "experiment") {
|
|
28
|
+
const outputDir = await runExperiment(command.configPath);
|
|
29
|
+
console.log(`Experiment results written to ${outputDir}`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
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));
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
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 = [];
|
|
93
|
+
const t = createTracer({
|
|
94
|
+
onEvent: (event) => {
|
|
95
|
+
traceEvents.push(event);
|
|
96
|
+
debugSemTrace(command, event);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const { value: changeSet, ms: diffMs } = await t.trace("entity.diff", () => semChangeSetForCommand(command), {
|
|
100
|
+
count: (v) => v.summary.total,
|
|
101
|
+
detail: (v) => `${v.summary.fileCount} files`,
|
|
102
|
+
});
|
|
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
|
+
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,
|
|
139
|
+
stats: {
|
|
140
|
+
filesChanged: changeSet.summary.fileCount,
|
|
141
|
+
additions: changeSet.summary.added,
|
|
142
|
+
deletions: changeSet.summary.deleted,
|
|
143
|
+
},
|
|
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,
|
|
157
|
+
},
|
|
158
|
+
warnings: [],
|
|
159
|
+
auditStats: result.stats,
|
|
160
|
+
debugTargets: command.debugTargets ? debugTargetsFromContexts(contexts, changeSet.label) : undefined,
|
|
161
|
+
traceEvents,
|
|
162
|
+
},
|
|
163
|
+
result,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await changeSet.cleanup();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
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;
|
|
235
|
+
}
|
|
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
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function countTargetsByCheck(candidates) {
|
|
253
|
+
const counts = {};
|
|
254
|
+
for (const candidate of candidates) {
|
|
255
|
+
counts[candidate.checkId] = (counts[candidate.checkId] ?? 0) + 1;
|
|
256
|
+
}
|
|
257
|
+
return counts;
|
|
258
|
+
}
|
|
259
|
+
function debugTargetsFromContexts(contexts, sourceLabel) {
|
|
260
|
+
return contexts.map((context) => ({
|
|
261
|
+
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,
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
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);
|
|
384
|
+
}
|
|
385
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
386
|
+
process.exitCode = await main();
|
|
387
|
+
}
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type TraceFields = Record<string, string | number | boolean | null | undefined>;
|
|
2
|
+
export type Tracer = {
|
|
3
|
+
trace<T>(span: string, fn: () => Promise<T>, options?: SpanTraceOptions<T>): Promise<{
|
|
4
|
+
value: T;
|
|
5
|
+
ms: number;
|
|
6
|
+
}>;
|
|
7
|
+
trace<T>(span: string, fn: () => T, options?: SpanTraceOptions<T>): {
|
|
8
|
+
value: T;
|
|
9
|
+
ms: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type SpanTraceEvent = Readonly<{
|
|
13
|
+
name: string;
|
|
14
|
+
ms: number;
|
|
15
|
+
count?: number;
|
|
16
|
+
detail?: string;
|
|
17
|
+
}>;
|
|
18
|
+
export type SpanTraceOptions<T> = Readonly<{
|
|
19
|
+
fields?: TraceFields;
|
|
20
|
+
count?: (value: T) => number;
|
|
21
|
+
detail?: (value: T) => string;
|
|
22
|
+
}>;
|
|
23
|
+
export type CreateTracerOptions = {
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
writeLine?: (line: string) => void;
|
|
26
|
+
onEvent?: (event: SpanTraceEvent) => void;
|
|
27
|
+
};
|
|
28
|
+
export declare function createTracer(options?: CreateTracerOptions): Tracer;
|
|
29
|
+
export declare const trace: Tracer;
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
export function createTracer(options) {
|
|
3
|
+
const enabled = options?.enabled ?? true;
|
|
4
|
+
const writeLine = options?.writeLine ?? ((line) => process.stderr.write(line + "\n"));
|
|
5
|
+
const onEvent = options?.onEvent;
|
|
6
|
+
const nowMs = () => performance.now();
|
|
7
|
+
function emit(span, durationMs, fields) {
|
|
8
|
+
if (!enabled)
|
|
9
|
+
return;
|
|
10
|
+
const payload = { span, ms: Math.round(durationMs) };
|
|
11
|
+
for (const [k, v] of Object.entries(fields ?? {})) {
|
|
12
|
+
if (v !== undefined)
|
|
13
|
+
payload[k] = v;
|
|
14
|
+
}
|
|
15
|
+
writeLine(`trace ${JSON.stringify(payload)}`);
|
|
16
|
+
}
|
|
17
|
+
function trace(span, fn, options) {
|
|
18
|
+
const startedAtMs = nowMs();
|
|
19
|
+
try {
|
|
20
|
+
const out = fn();
|
|
21
|
+
if (isPromiseLike(out)) {
|
|
22
|
+
return (async () => {
|
|
23
|
+
let durationMs;
|
|
24
|
+
try {
|
|
25
|
+
const value = await out;
|
|
26
|
+
durationMs = nowMs() - startedAtMs;
|
|
27
|
+
const event = {
|
|
28
|
+
name: span,
|
|
29
|
+
ms: Math.round(durationMs),
|
|
30
|
+
count: options?.count?.(value),
|
|
31
|
+
detail: options?.detail?.(value),
|
|
32
|
+
};
|
|
33
|
+
onEvent?.(event);
|
|
34
|
+
return { value, ms: event.ms };
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
durationMs ??= nowMs() - startedAtMs;
|
|
38
|
+
emit(span, durationMs, options?.fields);
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
}
|
|
42
|
+
const durationMs = nowMs() - startedAtMs;
|
|
43
|
+
emit(span, durationMs, options?.fields);
|
|
44
|
+
const event = {
|
|
45
|
+
name: span,
|
|
46
|
+
ms: Math.round(durationMs),
|
|
47
|
+
count: options?.count?.(out),
|
|
48
|
+
detail: options?.detail?.(out),
|
|
49
|
+
};
|
|
50
|
+
onEvent?.(event);
|
|
51
|
+
return { value: out, ms: event.ms };
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const durationMs = nowMs() - startedAtMs;
|
|
55
|
+
emit(span, durationMs, options?.fields);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { trace };
|
|
60
|
+
}
|
|
61
|
+
export const trace = createTracer();
|
|
62
|
+
function isPromiseLike(value) {
|
|
63
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
64
|
+
}
|