agentwaste-core 0.1.0

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/src/report.js ADDED
@@ -0,0 +1,544 @@
1
+ export function buildReport(stats, options) {
2
+ const color = colorizer(options.color);
3
+ const json = toReportJson(stats, options);
4
+ return {
5
+ json,
6
+ text: renderText(json, color),
7
+ };
8
+ }
9
+
10
+ export function toReportJson(stats, options) {
11
+ return {
12
+ version: options.version,
13
+ elapsed_ms: options.elapsedMs,
14
+ scanned: {
15
+ days: stats.days,
16
+ files: stats.files,
17
+ sessions: stats.sessions,
18
+ subagent_sessions: stats.subagentSessions,
19
+ sources: Object.fromEntries(Object.entries(stats.sources).map(([key, source]) => [key, sourceJson(source)])),
20
+ },
21
+ metrics: {
22
+ context_efficiency: {
23
+ level: stats.derived.contextOverheadLevel,
24
+ confidence: stats.derived.sampleConfidence,
25
+ active_tokens: stats.tokens.active,
26
+ fresh_input_tokens: stats.tokens.freshInput,
27
+ output_tokens: stats.tokens.output,
28
+ cached_input_tokens: stats.tokens.cached,
29
+ fresh_input_drag_percent: stats.derived.contextDragPercent,
30
+ cache_hit_percent: stats.derived.cacheHitPercent,
31
+ excess_fresh_input_percent: stats.derived.excessFreshInputPercent,
32
+ excess_fresh_input_tokens: stats.derived.excessFreshInputTokens,
33
+ active_tokens_per_tool_call: stats.derived.avgTokensPerToolCall,
34
+ fresh_input_tokens_per_tool_call: stats.derived.avgFreshInputPerToolCall,
35
+ review_threshold_fresh_input_tokens_per_tool_call: stats.derived.contextReviewFreshInputPerToolCall,
36
+ observed_model_calls: stats.tokens.observedModelCalls,
37
+ observed_tokens: stats.tokens.total,
38
+ },
39
+ credential_exposure: {
40
+ level: stats.derived.credentialRiskLevel,
41
+ score: stats.derived.credentialRiskScore,
42
+ unique_secret_fingerprints: stats.secrets.rawKeys,
43
+ model_facing_secret_fingerprints: stats.secrets.modelFacingKeys,
44
+ by_kind: stats.secrets.byKind,
45
+ },
46
+ tool_reliability: {
47
+ level: stats.derived.reliabilityLevel,
48
+ failure_rate_percent: stats.derived.retryRatePercent,
49
+ failed_tool_calls: stats.failedToolCalls,
50
+ tool_calls: stats.toolCalls,
51
+ avg_retries_per_failure: stats.derived.avgRetriesPerFailure,
52
+ retry_recovery_percent: stats.derived.retryRecoveryPercent,
53
+ unresolved_failures: stats.derived.unresolvedFailures,
54
+ },
55
+ state_continuity: {
56
+ level: stats.derived.stateContinuityLevel,
57
+ hard_breaks: stats.stateLoss.incidents,
58
+ hard_breaks_per_session: stats.derived.hardBreakRatePerSession,
59
+ aborted_turns: stats.stateLoss.abortedTurns,
60
+ compaction_signals: stats.stateLoss.compactSignals,
61
+ phrase_signals: stats.stateLoss.phraseSignals,
62
+ compactions_per_session: stats.derived.compactionsPerSession,
63
+ },
64
+ trace_coverage: {
65
+ level: stats.derived.traceCoverageLevel,
66
+ session_file_coverage_percent: stats.derived.traceCoveragePercent,
67
+ structured_trace_signals: stats.traces.signals,
68
+ session_log_files: stats.derived.sessionLogFiles,
69
+ traced_session_log_files: stats.derived.tracedSessionLogFiles,
70
+ },
71
+ automation_footprint: {
72
+ level: stats.derived.automationFootprintLevel,
73
+ lines: stats.glue.lines,
74
+ files: stats.glue.files,
75
+ roots: stats.glue.roots,
76
+ largest_files: stats.glue.scriptFiles.slice(0, 5),
77
+ },
78
+ },
79
+ monthly_impact_estimate: {
80
+ active_tokens_30d: stats.derived.monthlyTokens,
81
+ excess_fresh_input_tokens_30d: stats.derived.excessMonthlyFreshInputTokens,
82
+ excess_context_spend_low_usd: stats.derived.excessSpendLow,
83
+ excess_context_spend_high_usd: stats.derived.excessSpendHigh,
84
+ cost_assumption_usd_per_million_excess_input_tokens: stats.derived.costPerMillionTokens,
85
+ failure_recovery_minutes: stats.derived.failureRecoveryMinutes,
86
+ state_recovery_minutes: stats.derived.stateRecoveryMinutes,
87
+ automation_maintenance_minutes: stats.derived.automationMaintenanceMinutes,
88
+ debugging_hours: stats.derived.debuggingHours,
89
+ security_exposure: stats.derived.securityExposure,
90
+ },
91
+ recommendation: {
92
+ diagnosis: diagnosis(stats),
93
+ fixes: [
94
+ "move repeated execution into reviewed scripts or jobs",
95
+ "vault credentials instead of passing secrets through model context",
96
+ "add full traces, retries, artifacts, and durable state",
97
+ ],
98
+ },
99
+ evidence: {
100
+ secret_files_sample: stats.secrets.files,
101
+ state_continuity_files_sample: stats.stateLoss.files,
102
+ trace_files_sample: stats.traces.files,
103
+ diagnostics: stats.diagnostics.slice(0, 10),
104
+ },
105
+ };
106
+ }
107
+
108
+ function sourceJson(source) {
109
+ return {
110
+ label: source.label,
111
+ files: source.files,
112
+ sessions: source.sessions,
113
+ tool_calls: source.toolCalls,
114
+ failed_tool_calls: source.failedToolCalls,
115
+ tokens: source.tokens,
116
+ active_tokens: source.activeTokens,
117
+ fresh_input_tokens: source.freshInputTokens,
118
+ cached_input_tokens: source.cachedInputTokens,
119
+ secret_fingerprints: source.secrets,
120
+ model_facing_secret_fingerprints: source.modelFacingSecrets,
121
+ trace_signals: source.traces,
122
+ traced_session_log_files: source.tracedFiles,
123
+ retry_recovery_signals: source.retryAfterFailure,
124
+ unavailable: source.unavailable,
125
+ };
126
+ }
127
+
128
+ function renderText(report, color) {
129
+ const context = report.metrics.context_efficiency;
130
+ const credential = report.metrics.credential_exposure;
131
+ const reliability = report.metrics.tool_reliability;
132
+ const trace = report.metrics.trace_coverage;
133
+ const verdict = wasteVerdict(report);
134
+
135
+ const rows = [
136
+ reportText("agentmess postmortem", color.title("agentmess postmortem")),
137
+ reportText("──────────────────────────────", color.rule("──────────────────────────────")),
138
+ reportText(""),
139
+ reportSection("finding", color),
140
+ ...paragraphRows(agentFinding(report), color, { level: verdict.level }),
141
+ reportText(""),
142
+ reportSection("impact", color),
143
+ ...paragraphRows(agentImpact(report), color),
144
+ reportText(""),
145
+ reportSection("agent leaderboard", color),
146
+ ...agentLeaderboardRows(report, color),
147
+ reportText(""),
148
+ reportSection("numbers", color),
149
+ metricLine("sessions scanned", formatNumber(report.scanned.sessions), color),
150
+ metricLine("files scanned", formatNumber(report.scanned.files), color),
151
+ metricLine("fresh input/tool", `${formatNumber(context.fresh_input_tokens_per_tool_call)} tokens`, color, { level: context.level }),
152
+ metricLine("model-facing secrets", formatNumber(credential.model_facing_secret_fingerprints), color, { level: credential.model_facing_secret_fingerprints > 0 ? "CRITICAL" : "LOW" }),
153
+ metricLine("tool failure rate", `${reliability.failure_rate_percent}%`, color, { level: reliability.level }),
154
+ metricLine("trace coverage", `${trace.session_file_coverage_percent}%`, color, { level: trace.level }),
155
+ ];
156
+
157
+ return rows.map((row) => row.painted).join("\n");
158
+ }
159
+
160
+ function agentFinding(report) {
161
+ const context = report.metrics.context_efficiency;
162
+ const credential = report.metrics.credential_exposure;
163
+ const reliability = report.metrics.tool_reliability;
164
+ const trace = report.metrics.trace_coverage;
165
+ const findings = [];
166
+ if (credential.model_facing_secret_fingerprints > 0) findings.push("model-facing secret exposure");
167
+ if (context.fresh_input_tokens_per_tool_call > context.review_threshold_fresh_input_tokens_per_tool_call) findings.push("context above review threshold");
168
+ if (reliability.failed_tool_calls > 0) findings.push("tool-call failures");
169
+ if (trace.session_file_coverage_percent < 80) findings.push("low trace coverage");
170
+ if (findings.length === 0) return "No high-priority agent issues were detected in the scanned window.";
171
+ return `The scan found ${findings.join(", ")} in local agent activity.`;
172
+ }
173
+
174
+ function agentImpact(report) {
175
+ const credential = report.metrics.credential_exposure;
176
+ const reliability = report.metrics.tool_reliability;
177
+ const trace = report.metrics.trace_coverage;
178
+ const missingTraceLogs = Math.max(0, trace.session_log_files - trace.traced_session_log_files);
179
+ const parts = [];
180
+ if (credential.model_facing_secret_fingerprints > 0) parts.push(`${formatNumber(credential.model_facing_secret_fingerprints)} secret fingerprints reached model-facing records`);
181
+ if (reliability.unresolved_failures > 0) parts.push(`${formatNumber(reliability.unresolved_failures)} failed tool calls did not show recovery`);
182
+ if (missingTraceLogs > 0) parts.push(`${formatNumber(missingTraceLogs)} session logs may be hard to replay`);
183
+ if (parts.length === 0) return "The scanned agent activity looks operationally low-risk.";
184
+ return `${parts.join("; ")}.`;
185
+ }
186
+
187
+ export function calculationGuide(options = {}) {
188
+ const guide = {
189
+ version: options.version ?? "0.1.0",
190
+ formulas: {
191
+ context_efficiency: {
192
+ codex_fresh_input_tokens: "max(input_tokens - cached_input_tokens, 0)",
193
+ claude_fresh_input_tokens: "input_tokens + cache_creation_input_tokens",
194
+ active_tokens: "fresh_input_tokens + output_tokens",
195
+ fresh_input_tokens_per_tool_call: "sum(fresh_input_tokens) / tool_calls",
196
+ fresh_input_drag_percent: "fresh_input_tokens / active_tokens * 100",
197
+ cache_hit_percent: "cached_input_tokens / (fresh_input_tokens + cached_input_tokens) * 100",
198
+ excess_fresh_input_tokens: "max(0, fresh_input_tokens - tool_calls * 4000)",
199
+ note: "This is context overhead, not a universal waste benchmark. Cached reads are separated because prompt caching docs treat them as reusable prompt work.",
200
+ },
201
+ credential_exposure: {
202
+ raw_keys_found: "unique redacted fingerprints matched by known API-key patterns in scanned logs/configs",
203
+ keys_passed_to_model: "unique key fingerprints found in user/assistant/tool-call records that are model-facing",
204
+ level: "CRITICAL if any key is model-facing; HIGH if any raw key appears in logs/config; LOW otherwise",
205
+ },
206
+ tool_reliability: {
207
+ failure_rate_percent: "failed_tool_calls / tool_calls * 100",
208
+ avg_retries_per_failure: "retry_after_failure_signals / failed_tool_calls",
209
+ retry_recovery_percent: "retry_after_failure_signals / failed_tool_calls * 100",
210
+ unresolved_failures: "max(0, failed_tool_calls - retry_after_failure_signals)",
211
+ retry_after_failure_signal: "same tool signature appears again after a failed result",
212
+ },
213
+ state_continuity: {
214
+ hard_breaks: "aborted turns + model-facing text that mentions lost/forgotten context/state/history/memory",
215
+ compaction_signals: "summarization/compaction records counted separately because compaction is risk evidence, not always data loss",
216
+ hard_breaks_per_session: "hard_breaks / sessions",
217
+ },
218
+ trace_coverage: {
219
+ session_file_coverage_percent: "session log files with structured trace evidence / scanned session log files * 100",
220
+ structured_trace_signal: "tool text containing trace_id, span_id, run_id, OpenTelemetry, or otel",
221
+ },
222
+ automation_footprint: {
223
+ lines: "line count of likely local agent/tool automation files under scripts, bin, tools, .claude, .codex, .cursor, .agents, or .mcp",
224
+ roots: "top-level directories containing likely local automation files",
225
+ note: "This is a maintainability footprint. Lines are not inherently bad; scattered roots and large scripts raise review priority.",
226
+ },
227
+ monthly_impact_estimate: {
228
+ active_tokens_30d: "active_tokens scaled to 30 days",
229
+ excess_context_spend: "excess_fresh_input_tokens scaled to 30 days * explicit $3 per 1M input-token assumption, shown as a rough low/high range",
230
+ recovery_toil: "failed_tool_calls*3min + retry_signals*4min + hard_breaks*20min + automation_lines/35min",
231
+ },
232
+ },
233
+ };
234
+
235
+ if (options.json) return JSON.stringify(guide, null, 2);
236
+
237
+ return [
238
+ `agentmess calculation guide v${guide.version}`,
239
+ "",
240
+ "context efficiency",
241
+ " codex fresh input = max(input_tokens - cached_input_tokens, 0).",
242
+ " claude fresh input = input_tokens + cache_creation_input_tokens.",
243
+ " active tokens = fresh input + output tokens.",
244
+ " fresh input/tool call = sum(fresh input tokens) / tool_calls.",
245
+ " fresh-input drag % = fresh_input_tokens / active_tokens * 100.",
246
+ " cache hit % = cached_input_tokens / (fresh_input_tokens + cached_input_tokens) * 100.",
247
+ " excess context = max(0, fresh_input_tokens - tool_calls*4000).",
248
+ "",
249
+ "credential exposure",
250
+ " raw keys = unique redacted fingerprints matched by known api-key patterns.",
251
+ " model-facing keys = those fingerprints inside user/assistant/tool-call records.",
252
+ " level = critical for any model-facing secret, high for any raw secret in logs/config.",
253
+ "",
254
+ "tool reliability",
255
+ " failure rate = failed_tool_calls / tool_calls * 100.",
256
+ " avg retries/failure = retry-after-failure signals / failed_tool_calls.",
257
+ " retry recovery % = retry-after-failure signals / failed_tool_calls * 100.",
258
+ " a retry signal means the same tool signature appears again after a failure.",
259
+ "",
260
+ "state continuity",
261
+ " hard breaks = aborted turns + model-facing text mentioning lost/forgotten context, state, history, or memory.",
262
+ " compaction signals are reported separately because compaction can be healthy summarization or a risk signal.",
263
+ "",
264
+ "trace coverage",
265
+ " coverage = session log files with structured trace evidence / scanned session log files * 100.",
266
+ " trace signals include trace_id, span_id, run_id, opentelemetry, or otel.",
267
+ "",
268
+ "automation footprint",
269
+ " lines counted from likely local agent/tool automation files under scripts, bin, tools, .claude, .codex, .cursor, .agents, or .mcp.",
270
+ " roots count top-level directories containing those files.",
271
+ "",
272
+ "30-day impact estimate",
273
+ " active token burn = active tokens scaled to 30 days.",
274
+ " excess context spend = excess fresh input tokens scaled to 30 days * explicit $3 per 1m input-token assumption.",
275
+ " recovery/toil time = failures*3min + retry signals*4min + hard breaks*20min + automation_lines/35min.",
276
+ ].join("\n");
277
+ }
278
+
279
+ function diagnosis(stats) {
280
+ const highContextOverhead = stats.derived.contextOverheadLevel === "HIGH" || stats.derived.contextOverheadLevel === "CRITICAL";
281
+ const noTrace = stats.derived.traceCoverageLevel === "MISSING";
282
+ const riskySecrets = stats.derived.credentialRiskLevel === "HIGH" || stats.derived.credentialRiskLevel === "CRITICAL";
283
+ if (highContextOverhead && noTrace && riskySecrets) return "high operational risk. thrilling, in the wrong direction.";
284
+ if (highContextOverhead && noTrace) return "context-heavy and under-instrumented. a classic.";
285
+ if (riskySecrets) return "credential exposure detected. very senior behavior from the agents.";
286
+ return "mostly healthy. annoyingly competent.";
287
+ }
288
+
289
+ function scanWindow(days) {
290
+ return days === "all" ? "all sessions" : `last ${days}d`;
291
+ }
292
+
293
+ function conversationBox(title, rows, color) {
294
+ const width = Math.max(42, ...rows.map((row) => row.raw.length), title.length + 4);
295
+ const topFill = "─".repeat(Math.max(1, width - title.length - 1));
296
+ const bottomFill = "─".repeat(width + 2);
297
+ return [
298
+ `${color.border("╭─")} ${color.title(title)} ${color.border(`${topFill}╮`)}`,
299
+ ...rows.map((row) => boxRow(row, width, color)),
300
+ `${color.border("╰")}${color.border(bottomFill)}${color.border("╯")}`,
301
+ ].join("\n");
302
+ }
303
+
304
+ function boxRow(row, width, color) {
305
+ const padding = " ".repeat(Math.max(0, width - row.raw.length));
306
+ return `${color.border("│")} ${row.painted}${padding} ${color.border("│")}`;
307
+ }
308
+
309
+ function blankRow() {
310
+ return { raw: "", painted: "" };
311
+ }
312
+
313
+ function textRow(raw, painted = raw) {
314
+ return { raw, painted };
315
+ }
316
+
317
+ function reportText(raw, painted = raw) {
318
+ return { raw, painted: raw ? ` ${painted}` : "" };
319
+ }
320
+
321
+ function reportLine(label, value, color, options = {}) {
322
+ const labelWidth = options.labelWidth ?? 14;
323
+ const indent = " ".repeat(options.indent ?? 2);
324
+ const safeLabel = String(label).toLowerCase().slice(0, labelWidth);
325
+ const paintValue = options.solution ? color.solution(value) : options.level ? color.level(value, options.level) : color.value(value);
326
+ return {
327
+ raw: `${indent}${safeLabel.padEnd(labelWidth)} ${value}`,
328
+ painted: `${indent}${color.label(safeLabel.padEnd(labelWidth))} ${paintValue}`,
329
+ };
330
+ }
331
+
332
+ function metricLine(label, value, color, options = {}) {
333
+ return reportLine(label, value, color, { ...options, indent: 4, labelWidth: 28 });
334
+ }
335
+
336
+ function reportSection(label, color) {
337
+ return reportText(String(label).toLowerCase(), color.title(String(label).toLowerCase()));
338
+ }
339
+
340
+ function agentLeaderboardRows(report, color) {
341
+ const rows = rankAgentsOverall(report).slice(0, 5);
342
+ if (rows.length === 0) return paragraphRows("No agent had enough data for a leaderboard.", color);
343
+ const best = rows[0];
344
+ return rows.map((entry) => {
345
+ const winner = entry.score === best.score;
346
+ return leaderboardLine(winner ? `🏆 ${entry.label}` : ` ${entry.label}`, `${entry.score} risk pts · ${entry.summary}`, color, winner);
347
+ });
348
+ }
349
+
350
+ function rankAgentsOverall(report) {
351
+ return Object.values(report.scanned.sources ?? {})
352
+ .filter((source) => source.label !== "Agent config" && source.unavailable !== true && eligibleOverallAgent(source))
353
+ .map((source) => {
354
+ const traceCoverage = source.files > 0 ? ((source.traced_session_log_files ?? 0) / source.files) * 100 : 100;
355
+ const freshPerTool = source.tool_calls > 0 ? (source.fresh_input_tokens ?? 0) / source.tool_calls : 0;
356
+ const failureRate = source.tool_calls > 0 ? ((source.failed_tool_calls ?? 0) / source.tool_calls) * 100 : 0;
357
+ const unresolved = Math.max(0, (source.failed_tool_calls ?? 0) - (source.retry_recovery_signals ?? 0));
358
+ const score = Math.round(
359
+ Math.max(0, 100 - traceCoverage) * 0.25 +
360
+ Math.min(80, freshPerTool / 1000) +
361
+ failureRate * 1.5 +
362
+ Math.min(50, unresolved) +
363
+ (source.model_facing_secret_fingerprints ?? 0) * 20 +
364
+ (source.secret_fingerprints ?? 0) * 4,
365
+ );
366
+ return {
367
+ label: source.label,
368
+ score,
369
+ summary: agentSummary(source, traceCoverage, freshPerTool, failureRate),
370
+ };
371
+ })
372
+ .sort((a, b) => a.score - b.score || a.label.localeCompare(b.label));
373
+ }
374
+
375
+ function eligibleOverallAgent(source) {
376
+ return (source.sessions ?? 0) >= 20 || (source.tool_calls ?? 0) >= 100 || (source.files ?? 0) >= 100;
377
+ }
378
+
379
+ function agentSummary(source, traceCoverage, freshPerTool, failureRate) {
380
+ const parts = [];
381
+ if ((source.model_facing_secret_fingerprints ?? 0) > 0) parts.push(`${formatNumber(source.model_facing_secret_fingerprints)} model-facing secrets`);
382
+ if (failureRate > 0) parts.push(`${round(failureRate, 1)}% failures`);
383
+ if (freshPerTool > 0) parts.push(`${formatNumber(Math.round(freshPerTool))} fresh/tool`);
384
+ if (source.files > 0) parts.push(`${Math.round(traceCoverage)}% traces`);
385
+ return parts.slice(0, 2).join(" · ") || "cleanest scanned agent";
386
+ }
387
+
388
+ function leaderboardLine(label, value, color, winner) {
389
+ const safeLabel = shortText(label, 26).padEnd(26);
390
+ const paintLabel = winner ? color.solution(safeLabel) : color.label(safeLabel);
391
+ const paintValue = winner ? color.solution(value) : color.value(value);
392
+ return { raw: ` ${safeLabel} ${value}`, painted: ` ${paintLabel} ${paintValue}` };
393
+ }
394
+
395
+ function paragraphRows(value, color, options = {}) {
396
+ const indent = " ";
397
+ return wrapText(String(value), 76).map((line) => {
398
+ const paintValue = options.solution ? color.solution(line) : options.level ? color.level(line, options.level) : color.value(line);
399
+ return { raw: `${indent}${line}`, painted: `${indent}${paintValue}` };
400
+ });
401
+ }
402
+
403
+ function wrapText(value, width) {
404
+ const words = String(value).split(/\s+/).filter(Boolean);
405
+ const lines = [];
406
+ let line = "";
407
+ for (const word of words) {
408
+ if (!line) {
409
+ line = word;
410
+ continue;
411
+ }
412
+ if (`${line} ${word}`.length > width) {
413
+ lines.push(line);
414
+ line = word;
415
+ } else {
416
+ line = `${line} ${word}`;
417
+ }
418
+ }
419
+ if (line) lines.push(line);
420
+ return lines.length > 0 ? lines : [""];
421
+ }
422
+
423
+ function compactLine(label, value, color, options = {}) {
424
+ const raw = `${label.padEnd(10)} ${value}`;
425
+ const paintValue = options.solution ? color.solution(value) : options.level ? color.level(value, options.level) : color.value(value);
426
+ return { raw, painted: `${color.label(label.padEnd(10))} ${paintValue}` };
427
+ }
428
+
429
+ function solutionLine(report) {
430
+ const context = report.metrics.context_efficiency;
431
+ const credential = report.metrics.credential_exposure;
432
+ const reliability = report.metrics.tool_reliability;
433
+ const trace = report.metrics.trace_coverage;
434
+ const actions = [];
435
+
436
+ if (credential.unique_secret_fingerprints > 0) actions.push("rotate exposed credentials");
437
+ if (context.fresh_input_tokens_per_tool_call > context.review_threshold_fresh_input_tokens_per_tool_call) actions.push("reduce repeated context");
438
+ if (trace.session_file_coverage_percent < 80) actions.push("add trace/run IDs");
439
+ if (reliability.failed_tool_calls > 0) actions.push("classify retryable failures");
440
+
441
+ return actions.length > 0 ? actions.slice(0, 3).join(" · ") : "nothing urgent";
442
+ }
443
+
444
+ function wasteVerdict(report) {
445
+ const levels = [
446
+ report.metrics.context_efficiency.level,
447
+ report.metrics.credential_exposure.level,
448
+ report.metrics.tool_reliability.level,
449
+ report.metrics.trace_coverage.level,
450
+ ];
451
+ const score = levels.reduce((sum, level) => sum + levelWeight(level), 0);
452
+ if (score >= 8) return { label: "not good", level: "CRITICAL", line: "strong architecture review energy." };
453
+ if (score >= 4) return { label: "risky", level: "HIGH", line: "ship it only if you enjoy follow-up meetings." };
454
+ if (score >= 2) return { label: "mostly fine", level: "MEDIUM", line: "some review comments, not a rewrite." };
455
+ return { label: "healthy", level: "LOW", line: "i tried to complain. limited material." };
456
+ }
457
+
458
+ function shortText(value, max) {
459
+ const text = String(value ?? "");
460
+ if (text.length <= max) return text;
461
+ return `${text.slice(0, Math.max(0, max - 1))}…`;
462
+ }
463
+
464
+ function round(value, digits) {
465
+ const factor = 10 ** digits;
466
+ return Math.round(value * factor) / factor;
467
+ }
468
+
469
+ function levelWeight(level) {
470
+ switch (String(level).toLowerCase()) {
471
+ case "critical":
472
+ case "missing":
473
+ return 3;
474
+ case "high":
475
+ return 2;
476
+ case "medium":
477
+ case "partial":
478
+ return 1;
479
+ default:
480
+ return 0;
481
+ }
482
+ }
483
+
484
+ function formatNumber(value) {
485
+ return Number(value || 0).toLocaleString("en-US");
486
+ }
487
+
488
+ function formatSeconds(ms) {
489
+ const seconds = Math.max(0.1, ms / 1000);
490
+ return `${seconds.toFixed(seconds >= 10 ? 0 : 1)}s`;
491
+ }
492
+
493
+ function colorizer(enabled) {
494
+ if (!enabled) return plainColors();
495
+ const paint = (code, value) => `\x1b[${code}m${value}\x1b[0m`;
496
+ return {
497
+ accent: (value) => paint("38;2;147;197;253", value),
498
+ dim: (value) => paint("2", value),
499
+ border: (value) => paint("38;2;100;116;139", value),
500
+ label: (value) => paint("38;2;186;230;253", value),
501
+ muted: (value) => paint("38;2;148;163;184", value),
502
+ rule: (value) => paint("38;2;71;85;105", value),
503
+ solution: (value) => paint("38;2;125;211;252", value),
504
+ title: (value) => paint("38;2;147;197;253;1", value),
505
+ value: (value) => paint("38;2;226;232;240", value),
506
+ level: (value, level) => paint(levelColor(level), value),
507
+ };
508
+ }
509
+
510
+ function plainColors() {
511
+ return {
512
+ accent: (value) => value,
513
+ dim: (value) => value,
514
+ border: (value) => value,
515
+ label: (value) => value,
516
+ muted: (value) => value,
517
+ rule: (value) => value,
518
+ solution: (value) => value,
519
+ title: (value) => value,
520
+ value: (value) => value,
521
+ level: (value) => value,
522
+ };
523
+ }
524
+
525
+ function levelColor(level) {
526
+ switch (String(level).toLowerCase()) {
527
+ case "critical":
528
+ case "missing":
529
+ return "38;2;251;113;133";
530
+ case "high":
531
+ return "38;2;253;186;116";
532
+ case "medium":
533
+ case "partial":
534
+ return "38;2;147;197;253";
535
+ case "good":
536
+ case "low":
537
+ return "38;2;125;211;252";
538
+ case "config_only":
539
+ case "no_data":
540
+ return "38;2;148;163;184";
541
+ default:
542
+ return "90";
543
+ }
544
+ }