@stupify/cli 0.0.14 → 0.0.15
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/LICENSE +21 -0
- package/README.md +4 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/stupify.js +66 -4
- package/dist/trace.d.ts +2 -0
- package/dist/trace.js +22 -0
- package/package.json +6 -4
- package/src/constants.ts +1 -1
- package/src/stupify.ts +71 -14
- package/src/trace.ts +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stupify contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Local-only diagnostic CLI for checking whether AI is making you dumber.
|
|
4
4
|
|
|
5
|
+
Released under the MIT License.
|
|
6
|
+
|
|
5
7
|
Stupify has one analysis path:
|
|
6
8
|
|
|
7
9
|
```text
|
|
@@ -48,7 +50,8 @@ stupify --staged --max-search-input-tokens 24000
|
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
The package is prepared for the public `@stupify` npm scope. Publishing should
|
|
51
|
-
|
|
53
|
+
use the repository release workflow so npm receives Trusted Publishing
|
|
54
|
+
provenance. See the repository release docs.
|
|
52
55
|
|
|
53
56
|
This iteration intentionally does not run findings audit, validators, judges,
|
|
54
57
|
baselines, hosted LLM APIs, GitHub integration, dashboards, or repo-wide
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
package/dist/stupify.js
CHANGED
|
@@ -48,12 +48,32 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
export async function runSearchCommand(command, startedAt, ui = createCliUi({ quiet: command.json })) {
|
|
51
|
+
const activeSpans = new Map();
|
|
51
52
|
const t = createTracer({
|
|
52
53
|
writeLine: () => undefined,
|
|
53
54
|
onEvent: (event) => {
|
|
54
55
|
if (command.json)
|
|
55
56
|
return;
|
|
56
|
-
|
|
57
|
+
if (event.phase === "start") {
|
|
58
|
+
activeSpans.set(event.name, ui.spinner(formatStartStep(event.name, event.detail)));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const active = activeSpans.get(event.name);
|
|
62
|
+
activeSpans.delete(event.name);
|
|
63
|
+
const message = event.phase === "error"
|
|
64
|
+
? formatErrorStep(event.name, event.ms)
|
|
65
|
+
: formatStep(event.name, event.ms, event.count, event.detail);
|
|
66
|
+
if (!active) {
|
|
67
|
+
if (event.phase === "error")
|
|
68
|
+
ui.error(message);
|
|
69
|
+
else
|
|
70
|
+
ui.step(message);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (event.phase === "error")
|
|
74
|
+
active.error(message);
|
|
75
|
+
else
|
|
76
|
+
active.stop(message);
|
|
57
77
|
},
|
|
58
78
|
});
|
|
59
79
|
const profile = await loadSearchProfile(command.searchProfilePath);
|
|
@@ -143,7 +163,7 @@ export async function runSearchCommand(command, startedAt, ui = createCliUi({ qu
|
|
|
143
163
|
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
144
164
|
? initialPack
|
|
145
165
|
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
146
|
-
const batches = await buildSearchBatches({
|
|
166
|
+
const { value: batches } = await t.trace("search.batches", () => buildSearchBatches({
|
|
147
167
|
command,
|
|
148
168
|
changeSet,
|
|
149
169
|
contexts: searchContexts,
|
|
@@ -153,6 +173,12 @@ export async function runSearchCommand(command, startedAt, ui = createCliUi({ qu
|
|
|
153
173
|
includeCounterReasonInPrompt: command.includeCounterReasonInPrompt,
|
|
154
174
|
maxSearchInputTokens,
|
|
155
175
|
baseRepomixConfig,
|
|
176
|
+
}), {
|
|
177
|
+
startDetail: `${searchContexts.length} targets`,
|
|
178
|
+
count: (result) => result.batches.length,
|
|
179
|
+
detail: (result) => result.wasSplit
|
|
180
|
+
? `${result.skippedTargets} oversized targets skipped`
|
|
181
|
+
: `${result.estimatedInputTokens} estimated tokens`,
|
|
156
182
|
});
|
|
157
183
|
if (batches.batches.length === 0) {
|
|
158
184
|
return {
|
|
@@ -201,7 +227,10 @@ export async function runSearchCommand(command, startedAt, ui = createCliUi({ qu
|
|
|
201
227
|
let inputTokens = 0;
|
|
202
228
|
let exactSkippedTargets = batches.skippedTargets;
|
|
203
229
|
for (const batch of batches.batches) {
|
|
204
|
-
const batchInputTokens = await countPromptTokens(model, batch.request.prompt)
|
|
230
|
+
const { value: batchInputTokens } = await t.trace("prompt.tokens", () => countPromptTokens(model, batch.request.prompt), {
|
|
231
|
+
startDetail: `${batch.contexts.length} targets`,
|
|
232
|
+
count: (tokens) => tokens,
|
|
233
|
+
});
|
|
205
234
|
inputTokens += batchInputTokens;
|
|
206
235
|
if (batchInputTokens > maxSearchInputTokens) {
|
|
207
236
|
exactSkippedTargets += batch.contexts.length;
|
|
@@ -210,7 +239,10 @@ export async function runSearchCommand(command, startedAt, ui = createCliUi({ qu
|
|
|
210
239
|
}
|
|
211
240
|
continue;
|
|
212
241
|
}
|
|
213
|
-
const { value } = await t.trace("search.model", () => runSearch(model, batch.request), {
|
|
242
|
+
const { value } = await t.trace("search.model", () => runSearch(model, batch.request), {
|
|
243
|
+
startDetail: `${batch.contexts.length} targets`,
|
|
244
|
+
count: (v) => v.length,
|
|
245
|
+
});
|
|
214
246
|
modelCalls += 1;
|
|
215
247
|
matches.push(...withCheckWhy(value, checks));
|
|
216
248
|
}
|
|
@@ -344,15 +376,45 @@ function printRunPlan(command, patternIds, ui) {
|
|
|
344
376
|
`Patterns: ${patternIds.join(", ")}`,
|
|
345
377
|
].join("\n"), "Run");
|
|
346
378
|
}
|
|
379
|
+
function formatStartStep(name, detail) {
|
|
380
|
+
if (name === "entity.diff")
|
|
381
|
+
return "Diff: running sem over the selected git range";
|
|
382
|
+
if (name === "context.pack")
|
|
383
|
+
return "Context: packing selected target files with Repomix";
|
|
384
|
+
if (name === "search.batches")
|
|
385
|
+
return `Search: preparing token-bounded model batches${detail ? ` for ${detail}` : ""}`;
|
|
386
|
+
if (name === "prompt.tokens")
|
|
387
|
+
return `Tokens: counting search prompt${detail ? ` for ${detail}` : ""}`;
|
|
388
|
+
if (name === "search.model")
|
|
389
|
+
return `Model: searching selected target/check pairs${detail ? ` (${detail})` : ""}`;
|
|
390
|
+
return `${name}: working`;
|
|
391
|
+
}
|
|
347
392
|
function formatStep(name, ms, count, detail) {
|
|
348
393
|
if (name === "entity.diff")
|
|
349
394
|
return `Diff: ${detail ?? "changed files"}, ${count ?? 0} changed entities (${ms}ms)`;
|
|
350
395
|
if (name === "context.pack")
|
|
351
396
|
return `Context: ${count ?? 0} files, ${detail ?? "0 tokens"} (${ms}ms)`;
|
|
397
|
+
if (name === "search.batches")
|
|
398
|
+
return `Search: ${count ?? 0} model batches, ${detail ?? "0 estimated tokens"} (${ms}ms)`;
|
|
399
|
+
if (name === "prompt.tokens")
|
|
400
|
+
return `Tokens: ${count ?? 0} prompt tokens (${ms}ms)`;
|
|
352
401
|
if (name === "search.model")
|
|
353
402
|
return `Model: ${count ?? 0} matches (${ms}ms)`;
|
|
354
403
|
return `${name}: ${ms}ms`;
|
|
355
404
|
}
|
|
405
|
+
function formatErrorStep(name, ms) {
|
|
406
|
+
if (name === "entity.diff")
|
|
407
|
+
return `Diff failed after ${ms}ms`;
|
|
408
|
+
if (name === "context.pack")
|
|
409
|
+
return `Context packing failed after ${ms}ms`;
|
|
410
|
+
if (name === "search.batches")
|
|
411
|
+
return `Search batch preparation failed after ${ms}ms`;
|
|
412
|
+
if (name === "prompt.tokens")
|
|
413
|
+
return `Token counting failed after ${ms}ms`;
|
|
414
|
+
if (name === "search.model")
|
|
415
|
+
return `Model search failed after ${ms}ms`;
|
|
416
|
+
return `${name} failed after ${ms}ms`;
|
|
417
|
+
}
|
|
356
418
|
function scoutPlanLine(plan, entitiesScanned) {
|
|
357
419
|
if (plan.targets.length === 0) {
|
|
358
420
|
return `Scout: deterministic counters scanned ${entitiesScanned} entities; no target/check pairs selected`;
|
package/dist/trace.d.ts
CHANGED
|
@@ -11,12 +11,14 @@ export type Tracer = {
|
|
|
11
11
|
};
|
|
12
12
|
export type SpanTraceEvent = Readonly<{
|
|
13
13
|
name: string;
|
|
14
|
+
phase: "start" | "end" | "error";
|
|
14
15
|
ms: number;
|
|
15
16
|
count?: number;
|
|
16
17
|
detail?: string;
|
|
17
18
|
}>;
|
|
18
19
|
export type SpanTraceOptions<T> = Readonly<{
|
|
19
20
|
fields?: TraceFields;
|
|
21
|
+
startDetail?: string | (() => string);
|
|
20
22
|
count?: (value: T) => number;
|
|
21
23
|
detail?: (value: T) => string;
|
|
22
24
|
}>;
|
package/dist/trace.js
CHANGED
|
@@ -16,6 +16,12 @@ export function createTracer(options) {
|
|
|
16
16
|
}
|
|
17
17
|
function trace(span, fn, options) {
|
|
18
18
|
const startedAtMs = nowMs();
|
|
19
|
+
onEvent?.({
|
|
20
|
+
name: span,
|
|
21
|
+
phase: "start",
|
|
22
|
+
ms: 0,
|
|
23
|
+
detail: typeof options?.startDetail === "function" ? options.startDetail() : options?.startDetail,
|
|
24
|
+
});
|
|
19
25
|
try {
|
|
20
26
|
const out = fn();
|
|
21
27
|
if (isPromiseLike(out)) {
|
|
@@ -26,6 +32,7 @@ export function createTracer(options) {
|
|
|
26
32
|
durationMs = nowMs() - startedAtMs;
|
|
27
33
|
const event = {
|
|
28
34
|
name: span,
|
|
35
|
+
phase: "end",
|
|
29
36
|
ms: Math.round(durationMs),
|
|
30
37
|
count: options?.count?.(value),
|
|
31
38
|
detail: options?.detail?.(value),
|
|
@@ -33,6 +40,15 @@ export function createTracer(options) {
|
|
|
33
40
|
onEvent?.(event);
|
|
34
41
|
return { value, ms: event.ms };
|
|
35
42
|
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
durationMs = nowMs() - startedAtMs;
|
|
45
|
+
onEvent?.({
|
|
46
|
+
name: span,
|
|
47
|
+
phase: "error",
|
|
48
|
+
ms: Math.round(durationMs),
|
|
49
|
+
});
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
36
52
|
finally {
|
|
37
53
|
durationMs ??= nowMs() - startedAtMs;
|
|
38
54
|
emit(span, durationMs, options?.fields);
|
|
@@ -43,6 +59,7 @@ export function createTracer(options) {
|
|
|
43
59
|
emit(span, durationMs, options?.fields);
|
|
44
60
|
const event = {
|
|
45
61
|
name: span,
|
|
62
|
+
phase: "end",
|
|
46
63
|
ms: Math.round(durationMs),
|
|
47
64
|
count: options?.count?.(out),
|
|
48
65
|
detail: options?.detail?.(out),
|
|
@@ -52,6 +69,11 @@ export function createTracer(options) {
|
|
|
52
69
|
}
|
|
53
70
|
catch (error) {
|
|
54
71
|
const durationMs = nowMs() - startedAtMs;
|
|
72
|
+
onEvent?.({
|
|
73
|
+
name: span,
|
|
74
|
+
phase: "error",
|
|
75
|
+
ms: Math.round(durationMs),
|
|
76
|
+
});
|
|
55
77
|
emit(span, durationMs, options?.fields);
|
|
56
78
|
throw error;
|
|
57
79
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stupify/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "Local-only diagnostic CLI for checking whether AI is making you dumber.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "git+
|
|
12
|
+
"url": "git+https://github.com/Octember/stupif.ai.git",
|
|
13
13
|
"directory": "packages/cli"
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://stupif.ai",
|
|
@@ -23,16 +23,18 @@
|
|
|
23
23
|
"local-first",
|
|
24
24
|
"code-review"
|
|
25
25
|
],
|
|
26
|
-
"license": "
|
|
26
|
+
"license": "MIT",
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=20"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
31
|
+
"access": "public",
|
|
32
|
+
"provenance": true
|
|
32
33
|
},
|
|
33
34
|
"files": [
|
|
34
35
|
"dist",
|
|
35
36
|
"src",
|
|
37
|
+
"LICENSE",
|
|
36
38
|
"README.md",
|
|
37
39
|
"package.json"
|
|
38
40
|
],
|
package/src/constants.ts
CHANGED
package/src/stupify.ts
CHANGED
|
@@ -59,11 +59,28 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export async function runSearchCommand(command: SearchCommand, startedAt: number, ui = createCliUi({ quiet: command.json })): Promise<SearchRunJson> {
|
|
62
|
+
const activeSpans = new Map<string, ReturnType<CliUi["spinner"]>>();
|
|
62
63
|
const t = createTracer({
|
|
63
64
|
writeLine: () => undefined,
|
|
64
65
|
onEvent: (event) => {
|
|
65
66
|
if (command.json) return;
|
|
66
|
-
|
|
67
|
+
if (event.phase === "start") {
|
|
68
|
+
activeSpans.set(event.name, ui.spinner(formatStartStep(event.name, event.detail)));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const active = activeSpans.get(event.name);
|
|
73
|
+
activeSpans.delete(event.name);
|
|
74
|
+
const message = event.phase === "error"
|
|
75
|
+
? formatErrorStep(event.name, event.ms)
|
|
76
|
+
: formatStep(event.name, event.ms, event.count, event.detail);
|
|
77
|
+
if (!active) {
|
|
78
|
+
if (event.phase === "error") ui.error(message);
|
|
79
|
+
else ui.step(message);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.phase === "error") active.error(message);
|
|
83
|
+
else active.stop(message);
|
|
67
84
|
},
|
|
68
85
|
});
|
|
69
86
|
|
|
@@ -162,17 +179,27 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
162
179
|
const pack = profile?.context === "sem" || searchContexts.length === contexts.length
|
|
163
180
|
? initialPack
|
|
164
181
|
: await repomixContextPack(changeSet.contextCwd, searchContexts, changeSet.changes, baseRepomixConfig);
|
|
165
|
-
const batches = await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
const { value: batches } = await t.trace(
|
|
183
|
+
"search.batches",
|
|
184
|
+
() => buildSearchBatches({
|
|
185
|
+
command,
|
|
186
|
+
changeSet,
|
|
187
|
+
contexts: searchContexts,
|
|
188
|
+
initialPack: pack,
|
|
189
|
+
checks,
|
|
190
|
+
profile,
|
|
191
|
+
includeCounterReasonInPrompt: command.includeCounterReasonInPrompt,
|
|
192
|
+
maxSearchInputTokens,
|
|
193
|
+
baseRepomixConfig,
|
|
194
|
+
}),
|
|
195
|
+
{
|
|
196
|
+
startDetail: `${searchContexts.length} targets`,
|
|
197
|
+
count: (result) => result.batches.length,
|
|
198
|
+
detail: (result) => result.wasSplit
|
|
199
|
+
? `${result.skippedTargets} oversized targets skipped`
|
|
200
|
+
: `${result.estimatedInputTokens} estimated tokens`,
|
|
201
|
+
},
|
|
202
|
+
);
|
|
176
203
|
|
|
177
204
|
if (batches.batches.length === 0) {
|
|
178
205
|
return {
|
|
@@ -222,7 +249,14 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
222
249
|
let inputTokens = 0;
|
|
223
250
|
let exactSkippedTargets = batches.skippedTargets;
|
|
224
251
|
for (const batch of batches.batches) {
|
|
225
|
-
const batchInputTokens = await
|
|
252
|
+
const { value: batchInputTokens } = await t.trace(
|
|
253
|
+
"prompt.tokens",
|
|
254
|
+
() => countPromptTokens(model, batch.request.prompt),
|
|
255
|
+
{
|
|
256
|
+
startDetail: `${batch.contexts.length} targets`,
|
|
257
|
+
count: (tokens) => tokens,
|
|
258
|
+
},
|
|
259
|
+
);
|
|
226
260
|
inputTokens += batchInputTokens;
|
|
227
261
|
if (batchInputTokens > maxSearchInputTokens) {
|
|
228
262
|
exactSkippedTargets += batch.contexts.length;
|
|
@@ -234,7 +268,10 @@ export async function runSearchCommand(command: SearchCommand, startedAt: number
|
|
|
234
268
|
const { value } = await t.trace(
|
|
235
269
|
"search.model",
|
|
236
270
|
() => runSearch(model, batch.request),
|
|
237
|
-
{
|
|
271
|
+
{
|
|
272
|
+
startDetail: `${batch.contexts.length} targets`,
|
|
273
|
+
count: (v) => v.length,
|
|
274
|
+
},
|
|
238
275
|
);
|
|
239
276
|
modelCalls += 1;
|
|
240
277
|
matches.push(...withCheckWhy(value, checks));
|
|
@@ -441,13 +478,33 @@ function printRunPlan(
|
|
|
441
478
|
);
|
|
442
479
|
}
|
|
443
480
|
|
|
481
|
+
function formatStartStep(name: string, detail?: string): string {
|
|
482
|
+
if (name === "entity.diff") return "Diff: running sem over the selected git range";
|
|
483
|
+
if (name === "context.pack") return "Context: packing selected target files with Repomix";
|
|
484
|
+
if (name === "search.batches") return `Search: preparing token-bounded model batches${detail ? ` for ${detail}` : ""}`;
|
|
485
|
+
if (name === "prompt.tokens") return `Tokens: counting search prompt${detail ? ` for ${detail}` : ""}`;
|
|
486
|
+
if (name === "search.model") return `Model: searching selected target/check pairs${detail ? ` (${detail})` : ""}`;
|
|
487
|
+
return `${name}: working`;
|
|
488
|
+
}
|
|
489
|
+
|
|
444
490
|
function formatStep(name: string, ms: number, count?: number, detail?: string): string {
|
|
445
491
|
if (name === "entity.diff") return `Diff: ${detail ?? "changed files"}, ${count ?? 0} changed entities (${ms}ms)`;
|
|
446
492
|
if (name === "context.pack") return `Context: ${count ?? 0} files, ${detail ?? "0 tokens"} (${ms}ms)`;
|
|
493
|
+
if (name === "search.batches") return `Search: ${count ?? 0} model batches, ${detail ?? "0 estimated tokens"} (${ms}ms)`;
|
|
494
|
+
if (name === "prompt.tokens") return `Tokens: ${count ?? 0} prompt tokens (${ms}ms)`;
|
|
447
495
|
if (name === "search.model") return `Model: ${count ?? 0} matches (${ms}ms)`;
|
|
448
496
|
return `${name}: ${ms}ms`;
|
|
449
497
|
}
|
|
450
498
|
|
|
499
|
+
function formatErrorStep(name: string, ms: number): string {
|
|
500
|
+
if (name === "entity.diff") return `Diff failed after ${ms}ms`;
|
|
501
|
+
if (name === "context.pack") return `Context packing failed after ${ms}ms`;
|
|
502
|
+
if (name === "search.batches") return `Search batch preparation failed after ${ms}ms`;
|
|
503
|
+
if (name === "prompt.tokens") return `Token counting failed after ${ms}ms`;
|
|
504
|
+
if (name === "search.model") return `Model search failed after ${ms}ms`;
|
|
505
|
+
return `${name} failed after ${ms}ms`;
|
|
506
|
+
}
|
|
507
|
+
|
|
451
508
|
function scoutPlanLine(plan: CounterScoutPlan, entitiesScanned: number): string {
|
|
452
509
|
if (plan.targets.length === 0) {
|
|
453
510
|
return `Scout: deterministic counters scanned ${entitiesScanned} entities; no target/check pairs selected`;
|
package/src/trace.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type Tracer = {
|
|
|
9
9
|
|
|
10
10
|
export type SpanTraceEvent = Readonly<{
|
|
11
11
|
name: string;
|
|
12
|
+
phase: "start" | "end" | "error";
|
|
12
13
|
ms: number;
|
|
13
14
|
count?: number;
|
|
14
15
|
detail?: string;
|
|
@@ -16,6 +17,7 @@ export type SpanTraceEvent = Readonly<{
|
|
|
16
17
|
|
|
17
18
|
export type SpanTraceOptions<T> = Readonly<{
|
|
18
19
|
fields?: TraceFields;
|
|
20
|
+
startDetail?: string | (() => string);
|
|
19
21
|
count?: (value: T) => number;
|
|
20
22
|
detail?: (value: T) => string;
|
|
21
23
|
}>;
|
|
@@ -53,6 +55,12 @@ export function createTracer(options?: CreateTracerOptions): Tracer {
|
|
|
53
55
|
options?: SpanTraceOptions<T>,
|
|
54
56
|
): Promise<{ value: T; ms: number }> | { value: T; ms: number } {
|
|
55
57
|
const startedAtMs = nowMs();
|
|
58
|
+
onEvent?.({
|
|
59
|
+
name: span,
|
|
60
|
+
phase: "start",
|
|
61
|
+
ms: 0,
|
|
62
|
+
detail: typeof options?.startDetail === "function" ? options.startDetail() : options?.startDetail,
|
|
63
|
+
});
|
|
56
64
|
try {
|
|
57
65
|
const out = fn();
|
|
58
66
|
if (isPromiseLike(out)) {
|
|
@@ -63,12 +71,21 @@ export function createTracer(options?: CreateTracerOptions): Tracer {
|
|
|
63
71
|
durationMs = nowMs() - startedAtMs;
|
|
64
72
|
const event: SpanTraceEvent = {
|
|
65
73
|
name: span,
|
|
74
|
+
phase: "end",
|
|
66
75
|
ms: Math.round(durationMs),
|
|
67
76
|
count: options?.count?.(value),
|
|
68
77
|
detail: options?.detail?.(value),
|
|
69
78
|
};
|
|
70
79
|
onEvent?.(event);
|
|
71
80
|
return { value, ms: event.ms };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
durationMs = nowMs() - startedAtMs;
|
|
83
|
+
onEvent?.({
|
|
84
|
+
name: span,
|
|
85
|
+
phase: "error",
|
|
86
|
+
ms: Math.round(durationMs),
|
|
87
|
+
});
|
|
88
|
+
throw error;
|
|
72
89
|
} finally {
|
|
73
90
|
durationMs ??= nowMs() - startedAtMs;
|
|
74
91
|
emit(span, durationMs, options?.fields);
|
|
@@ -80,6 +97,7 @@ export function createTracer(options?: CreateTracerOptions): Tracer {
|
|
|
80
97
|
emit(span, durationMs, options?.fields);
|
|
81
98
|
const event: SpanTraceEvent = {
|
|
82
99
|
name: span,
|
|
100
|
+
phase: "end",
|
|
83
101
|
ms: Math.round(durationMs),
|
|
84
102
|
count: options?.count?.(out),
|
|
85
103
|
detail: options?.detail?.(out),
|
|
@@ -88,6 +106,11 @@ export function createTracer(options?: CreateTracerOptions): Tracer {
|
|
|
88
106
|
return { value: out, ms: event.ms };
|
|
89
107
|
} catch (error) {
|
|
90
108
|
const durationMs = nowMs() - startedAtMs;
|
|
109
|
+
onEvent?.({
|
|
110
|
+
name: span,
|
|
111
|
+
phase: "error",
|
|
112
|
+
ms: Math.round(durationMs),
|
|
113
|
+
});
|
|
91
114
|
emit(span, durationMs, options?.fields);
|
|
92
115
|
throw error;
|
|
93
116
|
}
|