inspecto 1.0.11 → 1.0.13
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 +101 -14
- package/dist/index.js +645 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -68,6 +68,7 @@ Expected: ${projectsDir}`
|
|
|
68
68
|
sessionId,
|
|
69
69
|
projectSlug: projectDir,
|
|
70
70
|
mtime: fileStat.mtime,
|
|
71
|
+
birthtime: fileStat.birthtime,
|
|
71
72
|
subagentPaths
|
|
72
73
|
};
|
|
73
74
|
} catch {
|
|
@@ -256,7 +257,7 @@ function normalizeContent(content) {
|
|
|
256
257
|
// src/metrics/reads-per-edit.ts
|
|
257
258
|
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "NotebookEdit"]);
|
|
258
259
|
var READ_TOOL = "Read";
|
|
259
|
-
function computeReadsPerEdit(session) {
|
|
260
|
+
function computeReadsPerEdit(session, thresholds) {
|
|
260
261
|
let readsSinceLastEdit = 0;
|
|
261
262
|
const ratios = [];
|
|
262
263
|
for (const turn of session.turns) {
|
|
@@ -282,10 +283,11 @@ function computeReadsPerEdit(session) {
|
|
|
282
283
|
};
|
|
283
284
|
}
|
|
284
285
|
const average2 = ratios.reduce((a, b) => a + b, 0) / ratios.length;
|
|
286
|
+
const { healthy, warning } = thresholds ?? { healthy: 4, warning: 2 };
|
|
285
287
|
return {
|
|
286
288
|
name: "reads-per-edit",
|
|
287
289
|
value: round(average2),
|
|
288
|
-
status: average2 >=
|
|
290
|
+
status: average2 >= healthy ? "healthy" : average2 >= warning ? "warning" : "critical",
|
|
289
291
|
label: round(average2).toString()
|
|
290
292
|
};
|
|
291
293
|
}
|
|
@@ -294,7 +296,7 @@ function round(n) {
|
|
|
294
296
|
}
|
|
295
297
|
|
|
296
298
|
// src/metrics/rewrite-ratio.ts
|
|
297
|
-
function computeRewriteRatio(session) {
|
|
299
|
+
function computeRewriteRatio(session, thresholds) {
|
|
298
300
|
let writes = 0;
|
|
299
301
|
let edits = 0;
|
|
300
302
|
for (const turn of session.turns) {
|
|
@@ -317,10 +319,11 @@ function computeRewriteRatio(session) {
|
|
|
317
319
|
};
|
|
318
320
|
}
|
|
319
321
|
const ratio = writes / total;
|
|
322
|
+
const { healthy, warning } = thresholds ?? { healthy: 0.25, warning: 0.5 };
|
|
320
323
|
return {
|
|
321
324
|
name: "rewrite-ratio",
|
|
322
325
|
value: round2(ratio),
|
|
323
|
-
status: ratio <=
|
|
326
|
+
status: ratio <= healthy ? "healthy" : ratio <= warning ? "warning" : "critical",
|
|
324
327
|
label: round2(ratio).toString()
|
|
325
328
|
};
|
|
326
329
|
}
|
|
@@ -329,7 +332,7 @@ function round2(n) {
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
// src/metrics/cache-hit-rate.ts
|
|
332
|
-
function computeCacheHitRate(session) {
|
|
335
|
+
function computeCacheHitRate(session, thresholds) {
|
|
333
336
|
let totalCacheRead = 0;
|
|
334
337
|
let totalCacheCreation = 0;
|
|
335
338
|
for (const turn of session.turns) {
|
|
@@ -348,10 +351,11 @@ function computeCacheHitRate(session) {
|
|
|
348
351
|
};
|
|
349
352
|
}
|
|
350
353
|
const rate = totalCacheRead / totalInput;
|
|
354
|
+
const { healthy, warning } = thresholds ?? { healthy: 0.5, warning: 0.2 };
|
|
351
355
|
return {
|
|
352
356
|
name: "cache-hit-rate",
|
|
353
357
|
value: round3(rate),
|
|
354
|
-
status: rate >=
|
|
358
|
+
status: rate >= healthy ? "healthy" : rate >= warning ? "warning" : "critical",
|
|
355
359
|
label: round3(rate).toString()
|
|
356
360
|
};
|
|
357
361
|
}
|
|
@@ -368,7 +372,7 @@ var INTENT_PATTERNS = [
|
|
|
368
372
|
/\bI'll (?:also |then )?(?:fix|add|create|implement|refactor|modify|change|write|edit|update)\b/i,
|
|
369
373
|
/\bI'm going to\b/i
|
|
370
374
|
];
|
|
371
|
-
function computeTaskCompletion(session) {
|
|
375
|
+
function computeTaskCompletion(session, thresholds) {
|
|
372
376
|
const assistantTurns = session.turns.filter(
|
|
373
377
|
(t) => t.role === "assistant" && t.complete
|
|
374
378
|
);
|
|
@@ -393,10 +397,11 @@ function computeTaskCompletion(session) {
|
|
|
393
397
|
};
|
|
394
398
|
}
|
|
395
399
|
const rate = 1 - unfulfilledIntents / totalIntents;
|
|
400
|
+
const { healthy, warning } = thresholds ?? { healthy: 0.9, warning: 0.7 };
|
|
396
401
|
return {
|
|
397
402
|
name: "task-completion",
|
|
398
403
|
value: round4(rate),
|
|
399
|
-
status: rate >=
|
|
404
|
+
status: rate >= healthy ? "healthy" : rate >= warning ? "warning" : "critical",
|
|
400
405
|
label: round4(rate).toString()
|
|
401
406
|
};
|
|
402
407
|
}
|
|
@@ -454,7 +459,7 @@ function normalizedSimilarity(a, b, maxLen = 200) {
|
|
|
454
459
|
}
|
|
455
460
|
|
|
456
461
|
// src/metrics/retry-density.ts
|
|
457
|
-
function computeRetryDensity(session) {
|
|
462
|
+
function computeRetryDensity(session, thresholds) {
|
|
458
463
|
const humanTexts = [];
|
|
459
464
|
for (const turn of session.turns) {
|
|
460
465
|
if (!turn.isHumanTurn) continue;
|
|
@@ -479,10 +484,11 @@ function computeRetryDensity(session) {
|
|
|
479
484
|
}
|
|
480
485
|
}
|
|
481
486
|
const density = retries / pairs;
|
|
487
|
+
const { healthy, warning } = thresholds ?? { healthy: 0.1, warning: 0.25 };
|
|
482
488
|
return {
|
|
483
489
|
name: "retry-density",
|
|
484
490
|
value: round5(density),
|
|
485
|
-
status: density <=
|
|
491
|
+
status: density <= healthy ? "healthy" : density <= warning ? "warning" : "critical",
|
|
486
492
|
label: round5(density).toString()
|
|
487
493
|
};
|
|
488
494
|
}
|
|
@@ -491,7 +497,7 @@ function round5(n) {
|
|
|
491
497
|
}
|
|
492
498
|
|
|
493
499
|
// src/metrics/tool-diversity.ts
|
|
494
|
-
function computeToolDiversity(session) {
|
|
500
|
+
function computeToolDiversity(session, thresholds) {
|
|
495
501
|
const toolCounts = /* @__PURE__ */ new Map();
|
|
496
502
|
for (const turn of session.turns) {
|
|
497
503
|
if (turn.role !== "assistant") continue;
|
|
@@ -523,10 +529,11 @@ function computeToolDiversity(session) {
|
|
|
523
529
|
const topTool = sorted[0];
|
|
524
530
|
const topPercent = Math.round(topTool[1] / totalCalls * 100);
|
|
525
531
|
const detail = `Most used: ${topTool[0]} (${topPercent}%)`;
|
|
532
|
+
const { healthy, warning } = thresholds ?? { healthy: 0.6, warning: 0.4 };
|
|
526
533
|
return {
|
|
527
534
|
name: "tool-diversity",
|
|
528
535
|
value: round6(normalized),
|
|
529
|
-
status: normalized >=
|
|
536
|
+
status: normalized >= healthy ? "healthy" : normalized >= warning ? "warning" : "critical",
|
|
530
537
|
label: round6(normalized).toString(),
|
|
531
538
|
detail
|
|
532
539
|
};
|
|
@@ -537,7 +544,7 @@ function round6(n) {
|
|
|
537
544
|
|
|
538
545
|
// src/metrics/tokens-per-edit.ts
|
|
539
546
|
var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["Write", "Edit", "NotebookEdit"]);
|
|
540
|
-
function computeTokensPerEdit(session) {
|
|
547
|
+
function computeTokensPerEdit(session, thresholds) {
|
|
541
548
|
let totalOutputTokens = 0;
|
|
542
549
|
let editCount = 0;
|
|
543
550
|
for (const turn of session.turns) {
|
|
@@ -561,10 +568,11 @@ function computeTokensPerEdit(session) {
|
|
|
561
568
|
};
|
|
562
569
|
}
|
|
563
570
|
const ratio = totalOutputTokens / editCount;
|
|
571
|
+
const { healthy, warning } = thresholds ?? { healthy: 5e3, warning: 15e3 };
|
|
564
572
|
return {
|
|
565
573
|
name: "tokens-per-edit",
|
|
566
574
|
value: Math.round(ratio),
|
|
567
|
-
status: ratio <=
|
|
575
|
+
status: ratio <= healthy ? "healthy" : ratio <= warning ? "warning" : "critical",
|
|
568
576
|
label: Math.round(ratio).toLocaleString("en-US")
|
|
569
577
|
};
|
|
570
578
|
}
|
|
@@ -611,57 +619,231 @@ function computeSubagentOverhead(session) {
|
|
|
611
619
|
};
|
|
612
620
|
}
|
|
613
621
|
|
|
614
|
-
// src/metrics/
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
// 0 ratio → 100, 0.25 → 50, 0.5+ → 0 (inverted: lower is better)
|
|
626
|
-
score: (v) => clamp((1 - v / 0.5) * 100, 0, 100)
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
compute: computeCacheHitRate,
|
|
630
|
-
weight: 0.15,
|
|
631
|
-
// 0% → 0, 50% → 100
|
|
632
|
-
score: (v) => clamp(v / 0.5 * 100, 0, 100)
|
|
633
|
-
},
|
|
634
|
-
{
|
|
635
|
-
compute: computeTaskCompletion,
|
|
636
|
-
weight: 0.15,
|
|
637
|
-
// 0.7 → 0, 0.9 → 50, 1.0 → 100
|
|
638
|
-
score: (v) => clamp((v - 0.7) / 0.3 * 100, 0, 100)
|
|
639
|
-
},
|
|
640
|
-
{
|
|
641
|
-
compute: computeRetryDensity,
|
|
642
|
-
weight: 0.1,
|
|
643
|
-
// 0% → 100, 10% → 60, 25%+ → 0 (inverted)
|
|
644
|
-
score: (v) => clamp((1 - v / 0.25) * 100, 0, 100)
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
compute: computeToolDiversity,
|
|
648
|
-
weight: 0.05,
|
|
649
|
-
// 0 → 0, 0.4 → 50, 0.6+ → 100
|
|
650
|
-
score: (v) => clamp(v / 0.6 * 100, 0, 100)
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
compute: computeTokensPerEdit,
|
|
654
|
-
weight: 0.15,
|
|
655
|
-
// 5000 → 100, 10000 → 50, 15000+ → 0 (inverted)
|
|
656
|
-
score: (v) => clamp((1 - (v - 5e3) / 1e4) * 100, 0, 100)
|
|
657
|
-
},
|
|
658
|
-
{
|
|
659
|
-
compute: computeSubagentOverhead,
|
|
660
|
-
weight: 0.05,
|
|
661
|
-
// main ratio 0 → 100, 0.6 → 100 (threshold), 0.8 → 50, 1.0 → 0 (inverted)
|
|
662
|
-
score: (v) => clamp((1 - v) / 0.4 * 100, 0, 100)
|
|
622
|
+
// src/metrics/tool-error-rate.ts
|
|
623
|
+
function computeToolErrorRate(session) {
|
|
624
|
+
let totalResults = 0;
|
|
625
|
+
let errorResults = 0;
|
|
626
|
+
for (const turn of session.turns) {
|
|
627
|
+
for (const block of turn.content) {
|
|
628
|
+
if (block.type !== "tool_result") continue;
|
|
629
|
+
const resultBlock = block;
|
|
630
|
+
totalResults++;
|
|
631
|
+
if (resultBlock.is_error === true) errorResults++;
|
|
632
|
+
}
|
|
663
633
|
}
|
|
664
|
-
|
|
634
|
+
if (totalResults === 0) {
|
|
635
|
+
return {
|
|
636
|
+
name: "tool-error-rate",
|
|
637
|
+
value: null,
|
|
638
|
+
status: "healthy",
|
|
639
|
+
label: "N/A",
|
|
640
|
+
detail: "No tool calls in this session"
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const rate = errorResults / totalResults;
|
|
644
|
+
return {
|
|
645
|
+
name: "tool-error-rate",
|
|
646
|
+
value: round7(rate),
|
|
647
|
+
status: rate <= 0.05 ? "healthy" : rate <= 0.15 ? "warning" : "critical",
|
|
648
|
+
label: `${(rate * 100).toFixed(1)}%`
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function round7(n) {
|
|
652
|
+
return Math.round(n * 1e4) / 1e4;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/metrics/thinking-utilization.ts
|
|
656
|
+
function computeThinkingUtilization(session) {
|
|
657
|
+
const assistantTurns = session.turns.filter((t) => t.role === "assistant");
|
|
658
|
+
const hasAnyToolUse = assistantTurns.some(
|
|
659
|
+
(t) => t.content.some((b) => b.type === "tool_use")
|
|
660
|
+
);
|
|
661
|
+
if (!hasAnyToolUse) {
|
|
662
|
+
return {
|
|
663
|
+
name: "thinking-utilization",
|
|
664
|
+
value: null,
|
|
665
|
+
status: "healthy",
|
|
666
|
+
label: "N/A",
|
|
667
|
+
detail: "No tool use in this session"
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const turnsWithThinking = assistantTurns.filter(
|
|
671
|
+
(t) => t.content.some((b) => b.type === "thinking")
|
|
672
|
+
).length;
|
|
673
|
+
if (turnsWithThinking === 0) {
|
|
674
|
+
return {
|
|
675
|
+
name: "thinking-utilization",
|
|
676
|
+
value: 0,
|
|
677
|
+
status: "critical",
|
|
678
|
+
label: "0%",
|
|
679
|
+
detail: "No extended thinking blocks detected"
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const ratio = Math.min(turnsWithThinking / assistantTurns.length, 1);
|
|
683
|
+
return {
|
|
684
|
+
name: "thinking-utilization",
|
|
685
|
+
value: round8(ratio),
|
|
686
|
+
status: ratio >= 0.3 ? "healthy" : ratio >= 0.1 ? "warning" : "critical",
|
|
687
|
+
label: `${(ratio * 100).toFixed(1)}%`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function round8(n) {
|
|
691
|
+
return Math.round(n * 1e4) / 1e4;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/metrics/mcp-usage.ts
|
|
695
|
+
function computeMcpUsage(session) {
|
|
696
|
+
const assistantTurnsWithTools = session.turns.filter(
|
|
697
|
+
(t) => t.role === "assistant" && t.content.some((b) => b.type === "tool_use")
|
|
698
|
+
);
|
|
699
|
+
if (assistantTurnsWithTools.length === 0) {
|
|
700
|
+
return {
|
|
701
|
+
name: "mcp-usage",
|
|
702
|
+
value: null,
|
|
703
|
+
status: "healthy",
|
|
704
|
+
label: "N/A",
|
|
705
|
+
detail: "No tool use in this session"
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
let mcpTurnCount = 0;
|
|
709
|
+
for (const turn of assistantTurnsWithTools) {
|
|
710
|
+
const hasMcpTool = turn.content.some(
|
|
711
|
+
(b) => b.type === "tool_use" && b.name.startsWith("mcp__")
|
|
712
|
+
);
|
|
713
|
+
const hasServerToolUse = (turn.usage?.server_tool_use?.web_search_requests ?? 0) > 0 || (turn.usage?.server_tool_use?.web_fetch_requests ?? 0) > 0;
|
|
714
|
+
if (hasMcpTool || hasServerToolUse) mcpTurnCount++;
|
|
715
|
+
}
|
|
716
|
+
const ratio = mcpTurnCount / assistantTurnsWithTools.length;
|
|
717
|
+
return {
|
|
718
|
+
name: "mcp-usage",
|
|
719
|
+
value: round9(ratio),
|
|
720
|
+
status: "healthy",
|
|
721
|
+
label: `${mcpTurnCount} turn${mcpTurnCount !== 1 ? "s" : ""}`
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function round9(n) {
|
|
725
|
+
return Math.round(n * 1e4) / 1e4;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/metrics/session-cost.ts
|
|
729
|
+
var PRICE_PER_M = {
|
|
730
|
+
output: 15,
|
|
731
|
+
cacheCreation: 3.75,
|
|
732
|
+
cacheRead: 0.3,
|
|
733
|
+
input: 3
|
|
734
|
+
};
|
|
735
|
+
function computeSessionCost(session, thresholds) {
|
|
736
|
+
let outputTokens = 0;
|
|
737
|
+
let cacheCreationTokens = 0;
|
|
738
|
+
let cacheReadTokens = 0;
|
|
739
|
+
for (const turn of session.turns) {
|
|
740
|
+
if (!turn.usage) continue;
|
|
741
|
+
outputTokens += turn.usage.output_tokens ?? 0;
|
|
742
|
+
cacheCreationTokens += turn.usage.cache_creation_input_tokens ?? 0;
|
|
743
|
+
cacheReadTokens += turn.usage.cache_read_input_tokens ?? 0;
|
|
744
|
+
}
|
|
745
|
+
const totalTokens = outputTokens + cacheCreationTokens + cacheReadTokens;
|
|
746
|
+
if (totalTokens === 0) {
|
|
747
|
+
return {
|
|
748
|
+
name: "session-cost",
|
|
749
|
+
value: null,
|
|
750
|
+
status: "healthy",
|
|
751
|
+
label: "N/A",
|
|
752
|
+
detail: "No token usage data in this session"
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const cost = (outputTokens * PRICE_PER_M.output + cacheCreationTokens * PRICE_PER_M.cacheCreation + cacheReadTokens * PRICE_PER_M.cacheRead) / 1e6;
|
|
756
|
+
const { healthy, warning } = thresholds ?? { healthy: 2, warning: 5 };
|
|
757
|
+
return {
|
|
758
|
+
name: "session-cost",
|
|
759
|
+
value: round10(cost),
|
|
760
|
+
status: cost <= healthy ? "healthy" : cost <= warning ? "warning" : "critical",
|
|
761
|
+
label: `$${cost.toFixed(2)}`
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function round10(n) {
|
|
765
|
+
return Math.round(n * 1e4) / 1e4;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/metrics/grader.ts
|
|
769
|
+
function buildMetricWeights(config2) {
|
|
770
|
+
const t = config2?.thresholds ?? {};
|
|
771
|
+
const w = config2?.weights ?? {};
|
|
772
|
+
return [
|
|
773
|
+
{
|
|
774
|
+
compute: (session) => computeReadsPerEdit(session, t.readsPerEdit),
|
|
775
|
+
weight: w.readsPerEdit ?? 0.14,
|
|
776
|
+
// 0 reads → 0, 2 reads → 50, 4+ reads → 100
|
|
777
|
+
score: (v) => clamp(v / 4 * 100, 0, 100)
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
compute: (session) => computeRewriteRatio(session, t.rewriteRatio),
|
|
781
|
+
weight: w.rewriteRatio ?? 0.11,
|
|
782
|
+
// 0 ratio → 100, 0.25 → 50, 0.5+ → 0 (inverted: lower is better)
|
|
783
|
+
score: (v) => clamp((1 - v / 0.5) * 100, 0, 100)
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
compute: (session) => computeCacheHitRate(session, t.cacheHitRate),
|
|
787
|
+
weight: w.cacheHitRate ?? 0.11,
|
|
788
|
+
// 0% → 0, 50% → 100
|
|
789
|
+
score: (v) => clamp(v / 0.5 * 100, 0, 100)
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
compute: (session) => computeTaskCompletion(session, t.taskCompletion),
|
|
793
|
+
weight: w.taskCompletion ?? 0.1,
|
|
794
|
+
// 0.7 → 0, 0.9 → 50, 1.0 → 100
|
|
795
|
+
score: (v) => clamp((v - 0.7) / 0.3 * 100, 0, 100)
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
compute: (session) => computeRetryDensity(session, t.retryDensity),
|
|
799
|
+
weight: w.retryDensity ?? 0.07,
|
|
800
|
+
// 0% → 100, 10% → 60, 25%+ → 0 (inverted)
|
|
801
|
+
score: (v) => clamp((1 - v / 0.25) * 100, 0, 100)
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
compute: (session) => computeToolDiversity(session, t.toolDiversity),
|
|
805
|
+
weight: w.toolDiversity ?? 0.06,
|
|
806
|
+
// 0 → 0, 0.4 → 50, 0.6+ → 100
|
|
807
|
+
score: (v) => clamp(v / 0.6 * 100, 0, 100)
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
compute: (session) => computeTokensPerEdit(session, t.tokensPerEdit),
|
|
811
|
+
weight: w.tokensPerEdit ?? 0.11,
|
|
812
|
+
// 5000 → 100, 10000 → 50, 15000+ → 0 (inverted)
|
|
813
|
+
score: (v) => clamp((1 - (v - 5e3) / 1e4) * 100, 0, 100)
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
compute: computeSubagentOverhead,
|
|
817
|
+
weight: 0.05,
|
|
818
|
+
// main ratio 0 → 100, 0.6 → 100 (threshold), 0.8 → 50, 1.0 → 0 (inverted)
|
|
819
|
+
score: (v) => clamp((1 - v) / 0.4 * 100, 0, 100)
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
compute: computeToolErrorRate,
|
|
823
|
+
weight: 0.08,
|
|
824
|
+
// 0% → 100, 5% → 83, 15% → 50, 30%+ → 0 (inverted: lower is better)
|
|
825
|
+
score: (v) => clamp((1 - v / 0.3) * 100, 0, 100)
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
compute: computeThinkingUtilization,
|
|
829
|
+
weight: 0.07,
|
|
830
|
+
// 0% → 0, 10% → 33, 30%+ → 100
|
|
831
|
+
score: (v) => clamp(v / 0.3 * 100, 0, 100)
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
compute: computeMcpUsage,
|
|
835
|
+
weight: 0.05,
|
|
836
|
+
// Informational: always contributes max score
|
|
837
|
+
score: (_v) => 100
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
compute: (session) => computeSessionCost(session, t.sessionCost),
|
|
841
|
+
weight: 0.05,
|
|
842
|
+
// $0 → 100, $5 → 50, $10+ → 0 (inverted: lower cost is better)
|
|
843
|
+
score: (v) => clamp((1 - v / 10) * 100, 0, 100)
|
|
844
|
+
}
|
|
845
|
+
];
|
|
846
|
+
}
|
|
665
847
|
var GRADE_THRESHOLDS = [
|
|
666
848
|
[97, "A+"],
|
|
667
849
|
[93, "A"],
|
|
@@ -680,11 +862,12 @@ var GRADE_THRESHOLDS = [
|
|
|
680
862
|
function gradeLetterFromScore(score) {
|
|
681
863
|
return GRADE_THRESHOLDS.find(([threshold]) => score >= threshold)?.[1] ?? "F";
|
|
682
864
|
}
|
|
683
|
-
function gradeSession(session) {
|
|
865
|
+
function gradeSession(session, config2) {
|
|
866
|
+
const metricWeights = buildMetricWeights(config2);
|
|
684
867
|
const metrics = [];
|
|
685
868
|
let weightedSum = 0;
|
|
686
869
|
let totalWeight = 0;
|
|
687
|
-
for (const mw of
|
|
870
|
+
for (const mw of metricWeights) {
|
|
688
871
|
const result = mw.compute(session);
|
|
689
872
|
metrics.push(result);
|
|
690
873
|
if (result.value !== null) {
|
|
@@ -756,6 +939,18 @@ var TIPS = {
|
|
|
756
939
|
"tokens-per-edit": {
|
|
757
940
|
warning: "Tokens per edit is above average. Claude may be verbose without being productive.",
|
|
758
941
|
critical: "Very high token cost per edit. Claude is burning tokens without proportional output."
|
|
942
|
+
},
|
|
943
|
+
"tool-error-rate": {
|
|
944
|
+
warning: "Elevated tool error rate. Claude may be calling tools with invalid arguments or on wrong paths.",
|
|
945
|
+
critical: "High tool error rate. Frequent tool failures indicate misaligned inputs or environment issues."
|
|
946
|
+
},
|
|
947
|
+
"thinking-utilization": {
|
|
948
|
+
warning: "Extended thinking is underutilized. Enable it in your Claude Code settings for complex tasks.",
|
|
949
|
+
critical: "No extended thinking detected on tool-using turns. Complex tasks benefit greatly from thinking blocks."
|
|
950
|
+
},
|
|
951
|
+
"session-cost": {
|
|
952
|
+
warning: "Session cost is above $2. Consider breaking long sessions into smaller focused tasks.",
|
|
953
|
+
critical: "Session cost exceeds $5. Review if Claude is re-reading large files or running redundant operations."
|
|
759
954
|
}
|
|
760
955
|
};
|
|
761
956
|
function getTip(metric) {
|
|
@@ -999,13 +1194,119 @@ function formatCacheCheckJson(results) {
|
|
|
999
1194
|
return JSON.stringify({ cacheCheck: results }, null, 2);
|
|
1000
1195
|
}
|
|
1001
1196
|
|
|
1197
|
+
// src/reporter/csv-reporter.ts
|
|
1198
|
+
function csvEscape(value) {
|
|
1199
|
+
if (value === null) return "";
|
|
1200
|
+
const str = String(value);
|
|
1201
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
1202
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
1203
|
+
}
|
|
1204
|
+
return str;
|
|
1205
|
+
}
|
|
1206
|
+
function csvRow(fields) {
|
|
1207
|
+
return fields.map(csvEscape).join(",");
|
|
1208
|
+
}
|
|
1209
|
+
function exportAuditCsv(grade) {
|
|
1210
|
+
const lines = [];
|
|
1211
|
+
lines.push(csvRow(["name", "value", "status", "label"]));
|
|
1212
|
+
for (const metric of grade.metrics) {
|
|
1213
|
+
lines.push(csvRow([metric.name, metric.value, metric.status, metric.label]));
|
|
1214
|
+
}
|
|
1215
|
+
return lines.join("\n");
|
|
1216
|
+
}
|
|
1217
|
+
function exportTrendCsv(results) {
|
|
1218
|
+
const lines = [];
|
|
1219
|
+
lines.push(csvRow(["name", "recentAvg", "fullAvg", "changePercent", "status"]));
|
|
1220
|
+
for (const r of results) {
|
|
1221
|
+
lines.push(csvRow([r.name, r.recentAvg, r.fullAvg, r.changePercent, r.status]));
|
|
1222
|
+
}
|
|
1223
|
+
return lines.join("\n");
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/config/loader.ts
|
|
1227
|
+
import { readFileSync } from "fs";
|
|
1228
|
+
import { join as join3 } from "path";
|
|
1229
|
+
var KNOWN_THRESHOLD_KEYS = /* @__PURE__ */ new Set([
|
|
1230
|
+
"readsPerEdit",
|
|
1231
|
+
"rewriteRatio",
|
|
1232
|
+
"cacheHitRate",
|
|
1233
|
+
"taskCompletion",
|
|
1234
|
+
"retryDensity",
|
|
1235
|
+
"toolDiversity",
|
|
1236
|
+
"tokensPerEdit",
|
|
1237
|
+
"sessionCost"
|
|
1238
|
+
]);
|
|
1239
|
+
var KNOWN_WEIGHT_KEYS = /* @__PURE__ */ new Set([
|
|
1240
|
+
"readsPerEdit",
|
|
1241
|
+
"rewriteRatio",
|
|
1242
|
+
"cacheHitRate",
|
|
1243
|
+
"taskCompletion",
|
|
1244
|
+
"retryDensity",
|
|
1245
|
+
"toolDiversity",
|
|
1246
|
+
"tokensPerEdit"
|
|
1247
|
+
]);
|
|
1248
|
+
var KNOWN_TOP_KEYS = /* @__PURE__ */ new Set(["thresholds", "weights", "dataDir", "defaultProject"]);
|
|
1249
|
+
function loadConfig() {
|
|
1250
|
+
const configPath = join3(process.cwd(), ".inspecto.json");
|
|
1251
|
+
let raw;
|
|
1252
|
+
try {
|
|
1253
|
+
raw = readFileSync(configPath, "utf8");
|
|
1254
|
+
} catch {
|
|
1255
|
+
return {};
|
|
1256
|
+
}
|
|
1257
|
+
let parsed;
|
|
1258
|
+
try {
|
|
1259
|
+
parsed = JSON.parse(raw);
|
|
1260
|
+
} catch {
|
|
1261
|
+
process.stderr.write("inspecto: warning: .inspecto.json contains invalid JSON, using defaults\n");
|
|
1262
|
+
return {};
|
|
1263
|
+
}
|
|
1264
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1265
|
+
process.stderr.write("inspecto: warning: .inspecto.json must be a JSON object, using defaults\n");
|
|
1266
|
+
return {};
|
|
1267
|
+
}
|
|
1268
|
+
const config2 = parsed;
|
|
1269
|
+
for (const key of Object.keys(config2)) {
|
|
1270
|
+
if (!KNOWN_TOP_KEYS.has(key)) {
|
|
1271
|
+
process.stderr.write(`inspecto: warning: .inspecto.json unknown key "${key}"
|
|
1272
|
+
`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (config2["thresholds"] && typeof config2["thresholds"] === "object" && !Array.isArray(config2["thresholds"])) {
|
|
1276
|
+
for (const key of Object.keys(config2["thresholds"])) {
|
|
1277
|
+
if (!KNOWN_THRESHOLD_KEYS.has(key)) {
|
|
1278
|
+
process.stderr.write(`inspecto: warning: .inspecto.json unknown thresholds key "${key}"
|
|
1279
|
+
`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (config2["weights"] && typeof config2["weights"] === "object" && !Array.isArray(config2["weights"])) {
|
|
1284
|
+
for (const key of Object.keys(config2["weights"])) {
|
|
1285
|
+
if (!KNOWN_WEIGHT_KEYS.has(key)) {
|
|
1286
|
+
process.stderr.write(`inspecto: warning: .inspecto.json unknown weights key "${key}"
|
|
1287
|
+
`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const weights = config2["weights"];
|
|
1291
|
+
const numericValues = Object.values(weights).filter((v) => typeof v === "number");
|
|
1292
|
+
const sum = numericValues.reduce((a, b) => a + b, 0);
|
|
1293
|
+
if (sum > 1 + 1e-9) {
|
|
1294
|
+
process.stderr.write(
|
|
1295
|
+
`inspecto: warning: weights in .inspecto.json sum to ${sum.toFixed(4)}, which exceeds 1.0
|
|
1296
|
+
`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return parsed;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1002
1303
|
// src/commands/audit.ts
|
|
1003
1304
|
var KNOWN_FORMAT_VERSION = "2.1.167";
|
|
1004
1305
|
async function runAudit(options) {
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
});
|
|
1306
|
+
const config2 = loadConfig();
|
|
1307
|
+
const dataDir = options.dataDir ?? config2.dataDir;
|
|
1308
|
+
const project = options.project ?? config2.defaultProject;
|
|
1309
|
+
const sessionFile = await getMostRecentSession({ dataDir, project });
|
|
1009
1310
|
const records = readJsonl(sessionFile.path);
|
|
1010
1311
|
const session = await buildSession(
|
|
1011
1312
|
records,
|
|
@@ -1013,24 +1314,29 @@ async function runAudit(options) {
|
|
|
1013
1314
|
sessionFile.projectSlug,
|
|
1014
1315
|
sessionFile.subagentPaths
|
|
1015
1316
|
);
|
|
1016
|
-
const grade = gradeSession(session);
|
|
1017
|
-
if (options.json) {
|
|
1317
|
+
const grade = gradeSession(session, config2);
|
|
1318
|
+
if (options.json || options.format === "json") {
|
|
1018
1319
|
console.log(formatAuditJson(session, grade));
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1320
|
+
} else if (options.format === "csv") {
|
|
1321
|
+
console.log(exportAuditCsv(grade));
|
|
1322
|
+
} else {
|
|
1323
|
+
if (session.formatVersion && session.formatVersion !== KNOWN_FORMAT_VERSION) {
|
|
1324
|
+
console.log(
|
|
1325
|
+
chalk2.yellow(
|
|
1326
|
+
`\u26A0 JSONL format version ${session.formatVersion} detected (expected ${KNOWN_FORMAT_VERSION}). Metrics may be inaccurate.`
|
|
1327
|
+
)
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
if (session.unknownRecordTypes.size > 0) {
|
|
1331
|
+
const types = [...session.unknownRecordTypes].sort().join(", ");
|
|
1332
|
+
process.stdout.write(chalk2.dim(`Note: skipped unknown record types: ${types}
|
|
1031
1333
|
`));
|
|
1334
|
+
}
|
|
1335
|
+
console.log(renderAuditReport(session, grade));
|
|
1336
|
+
}
|
|
1337
|
+
if (options.fail !== false && grade.score < 67) {
|
|
1338
|
+
process.exitCode = 1;
|
|
1032
1339
|
}
|
|
1033
|
-
console.log(renderAuditReport(session, grade));
|
|
1034
1340
|
}
|
|
1035
1341
|
|
|
1036
1342
|
// src/anomaly/baseline.ts
|
|
@@ -1173,13 +1479,12 @@ function setCachedGrade(sessionPath, mtime, grade) {
|
|
|
1173
1479
|
// src/commands/trend.ts
|
|
1174
1480
|
var CONCURRENCY = 16;
|
|
1175
1481
|
async function runTrend(options) {
|
|
1482
|
+
const config2 = loadConfig();
|
|
1483
|
+
const dataDir = options.dataDir ?? config2.dataDir;
|
|
1484
|
+
const project = options.project ?? config2.defaultProject;
|
|
1176
1485
|
const duration = options.since ?? "7d";
|
|
1177
1486
|
const sinceDate = parseDuration(duration);
|
|
1178
|
-
const sessionFiles = await scanSessions({
|
|
1179
|
-
dataDir: options.dataDir,
|
|
1180
|
-
project: options.project,
|
|
1181
|
-
since: sinceDate
|
|
1182
|
-
});
|
|
1487
|
+
const sessionFiles = await scanSessions({ dataDir, project, since: sinceDate });
|
|
1183
1488
|
if (sessionFiles.length === 0) {
|
|
1184
1489
|
console.log(`No sessions found in the last ${duration}.`);
|
|
1185
1490
|
return;
|
|
@@ -1189,7 +1494,7 @@ async function runTrend(options) {
|
|
|
1189
1494
|
if (cached) return cached;
|
|
1190
1495
|
const records = readJsonl(sf.path);
|
|
1191
1496
|
const session = await buildSession(records, sf.sessionId, sf.projectSlug, sf.subagentPaths);
|
|
1192
|
-
const grade = gradeSession(session);
|
|
1497
|
+
const grade = gradeSession(session, config2);
|
|
1193
1498
|
setCachedGrade(sf.path, sf.mtime, grade);
|
|
1194
1499
|
return grade;
|
|
1195
1500
|
});
|
|
@@ -1201,11 +1506,16 @@ async function runTrend(options) {
|
|
|
1201
1506
|
const recentCount = Math.max(1, Math.floor(grades.length / 2));
|
|
1202
1507
|
const baselines = computeBaselines(grades, recentCount);
|
|
1203
1508
|
const regressions = detectRegressions(baselines);
|
|
1204
|
-
if (options.json) {
|
|
1509
|
+
if (options.json || options.format === "json") {
|
|
1205
1510
|
console.log(formatTrendJson(regressions));
|
|
1511
|
+
} else if (options.format === "csv") {
|
|
1512
|
+
console.log(exportTrendCsv(regressions));
|
|
1206
1513
|
} else {
|
|
1207
1514
|
console.log(renderTrendReport(regressions, grades.length, duration));
|
|
1208
1515
|
}
|
|
1516
|
+
if (options.fail !== false && regressions.some((r) => r.status === "regression")) {
|
|
1517
|
+
process.exitCode = 1;
|
|
1518
|
+
}
|
|
1209
1519
|
}
|
|
1210
1520
|
|
|
1211
1521
|
// src/anomaly/cache-anomaly.ts
|
|
@@ -1257,6 +1567,9 @@ async function runCacheCheck(options) {
|
|
|
1257
1567
|
} else {
|
|
1258
1568
|
console.log(renderCacheCheckReport(results));
|
|
1259
1569
|
}
|
|
1570
|
+
if (options.fail !== false && results.some((r) => r.isAnomaly)) {
|
|
1571
|
+
process.exitCode = 1;
|
|
1572
|
+
}
|
|
1260
1573
|
}
|
|
1261
1574
|
|
|
1262
1575
|
// src/commands/compare.ts
|
|
@@ -1264,14 +1577,13 @@ import chalk3 from "chalk";
|
|
|
1264
1577
|
import Table2 from "cli-table3";
|
|
1265
1578
|
var CONCURRENCY2 = 16;
|
|
1266
1579
|
async function runCompare(options) {
|
|
1580
|
+
const config2 = loadConfig();
|
|
1581
|
+
const dataDir = options.dataDir ?? config2.dataDir;
|
|
1267
1582
|
const projectNames = options.projects.split(",").map((p) => p.trim());
|
|
1268
1583
|
const summaries = [];
|
|
1269
1584
|
const projectSummaries = await Promise.all(
|
|
1270
1585
|
projectNames.map(async (projectFilter) => {
|
|
1271
|
-
const sessionFiles = await scanSessions({
|
|
1272
|
-
dataDir: options.dataDir,
|
|
1273
|
-
project: projectFilter
|
|
1274
|
-
});
|
|
1586
|
+
const sessionFiles = await scanSessions({ dataDir, project: projectFilter });
|
|
1275
1587
|
if (sessionFiles.length === 0) return null;
|
|
1276
1588
|
const settled = await concurrentSettled(
|
|
1277
1589
|
sessionFiles,
|
|
@@ -1286,7 +1598,7 @@ async function runCompare(options) {
|
|
|
1286
1598
|
sf.projectSlug,
|
|
1287
1599
|
sf.subagentPaths
|
|
1288
1600
|
);
|
|
1289
|
-
const grade = gradeSession(session);
|
|
1601
|
+
const grade = gradeSession(session, config2);
|
|
1290
1602
|
setCachedGrade(sf.path, sf.mtime, grade);
|
|
1291
1603
|
return grade;
|
|
1292
1604
|
}
|
|
@@ -1372,37 +1684,269 @@ async function runCompare(options) {
|
|
|
1372
1684
|
console.log(lines.join("\n"));
|
|
1373
1685
|
}
|
|
1374
1686
|
|
|
1687
|
+
// src/commands/list.ts
|
|
1688
|
+
import chalk4 from "chalk";
|
|
1689
|
+
import Table3 from "cli-table3";
|
|
1690
|
+
var TABLE_CHARS = {
|
|
1691
|
+
top: "\u2500",
|
|
1692
|
+
"top-mid": "\u2500",
|
|
1693
|
+
"top-left": " ",
|
|
1694
|
+
"top-right": "",
|
|
1695
|
+
bottom: "\u2500",
|
|
1696
|
+
"bottom-mid": "\u2500",
|
|
1697
|
+
"bottom-left": " ",
|
|
1698
|
+
"bottom-right": "",
|
|
1699
|
+
left: " ",
|
|
1700
|
+
"left-mid": " ",
|
|
1701
|
+
mid: "\u2500",
|
|
1702
|
+
"mid-mid": "\u2500",
|
|
1703
|
+
right: "",
|
|
1704
|
+
"right-mid": "",
|
|
1705
|
+
middle: " "
|
|
1706
|
+
};
|
|
1707
|
+
var TABLE_STYLE = { head: [], border: [], "padding-left": 2, "padding-right": 2 };
|
|
1708
|
+
async function runList(options) {
|
|
1709
|
+
const sessionFiles = await scanSessions({
|
|
1710
|
+
dataDir: options.dataDir,
|
|
1711
|
+
project: options.project
|
|
1712
|
+
});
|
|
1713
|
+
if (sessionFiles.length === 0) {
|
|
1714
|
+
console.log("No sessions found.");
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (options.sessions || options.project) {
|
|
1718
|
+
const toShow = options.project ? sessionFiles : sessionFiles.slice(0, 20);
|
|
1719
|
+
const lines = [""];
|
|
1720
|
+
if (options.project) {
|
|
1721
|
+
lines.push(chalk4.bold(` Sessions for ${options.project}`) + chalk4.dim(` (${toShow.length})`));
|
|
1722
|
+
} else {
|
|
1723
|
+
lines.push(chalk4.bold(" 20 most recent sessions"));
|
|
1724
|
+
}
|
|
1725
|
+
lines.push("");
|
|
1726
|
+
const table = new Table3({
|
|
1727
|
+
head: ["Session", "Project", "Date", "Duration"].map((h) => chalk4.dim(h)),
|
|
1728
|
+
style: TABLE_STYLE,
|
|
1729
|
+
chars: TABLE_CHARS
|
|
1730
|
+
});
|
|
1731
|
+
for (const sf of toShow) {
|
|
1732
|
+
const dateStr = sf.mtime.toISOString().slice(0, 10);
|
|
1733
|
+
let durStr = "\u2014";
|
|
1734
|
+
if (sf.birthtime && sf.birthtime.getTime() > 0 && sf.birthtime.getTime() < sf.mtime.getTime()) {
|
|
1735
|
+
durStr = formatDuration(sf.mtime.getTime() - sf.birthtime.getTime());
|
|
1736
|
+
}
|
|
1737
|
+
table.push([shortSessionId(sf.sessionId), sf.projectSlug, dateStr, durStr]);
|
|
1738
|
+
}
|
|
1739
|
+
lines.push(table.toString());
|
|
1740
|
+
lines.push("");
|
|
1741
|
+
console.log(lines.join("\n"));
|
|
1742
|
+
} else {
|
|
1743
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
1744
|
+
for (const sf of sessionFiles) {
|
|
1745
|
+
const existing = projectMap.get(sf.projectSlug);
|
|
1746
|
+
if (!existing) {
|
|
1747
|
+
projectMap.set(sf.projectSlug, { count: 1, lastActive: sf.mtime });
|
|
1748
|
+
} else {
|
|
1749
|
+
existing.count++;
|
|
1750
|
+
if (sf.mtime > existing.lastActive) existing.lastActive = sf.mtime;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const projects = [...projectMap.entries()].sort(
|
|
1754
|
+
(a, b) => b[1].lastActive.getTime() - a[1].lastActive.getTime()
|
|
1755
|
+
);
|
|
1756
|
+
const lines = [""];
|
|
1757
|
+
lines.push(chalk4.bold(" Projects") + chalk4.dim(` (${projects.length} found)`));
|
|
1758
|
+
lines.push("");
|
|
1759
|
+
const table = new Table3({
|
|
1760
|
+
head: ["Project", "Sessions", "Last active"].map((h) => chalk4.dim(h)),
|
|
1761
|
+
style: TABLE_STYLE,
|
|
1762
|
+
chars: TABLE_CHARS
|
|
1763
|
+
});
|
|
1764
|
+
for (const [slug, info] of projects) {
|
|
1765
|
+
table.push([slug, info.count.toString(), info.lastActive.toISOString().slice(0, 10)]);
|
|
1766
|
+
}
|
|
1767
|
+
lines.push(table.toString());
|
|
1768
|
+
lines.push("");
|
|
1769
|
+
console.log(lines.join("\n"));
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// src/commands/config-validate.ts
|
|
1774
|
+
import chalk5 from "chalk";
|
|
1775
|
+
import Table4 from "cli-table3";
|
|
1776
|
+
|
|
1777
|
+
// src/config/types.ts
|
|
1778
|
+
var DEFAULT_THRESHOLDS = {
|
|
1779
|
+
readsPerEdit: { healthy: 4, warning: 2 },
|
|
1780
|
+
rewriteRatio: { healthy: 0.25, warning: 0.4 },
|
|
1781
|
+
cacheHitRate: { healthy: 0.5, warning: 0.3 },
|
|
1782
|
+
taskCompletion: { healthy: 0.9, warning: 0.7 },
|
|
1783
|
+
retryDensity: { healthy: 0.1, warning: 0.2 },
|
|
1784
|
+
toolDiversity: { healthy: 0.6, warning: 0.4 },
|
|
1785
|
+
tokensPerEdit: { healthy: 5e3, warning: 1e4 },
|
|
1786
|
+
sessionCost: { healthy: 2, warning: 5 }
|
|
1787
|
+
};
|
|
1788
|
+
var DEFAULT_WEIGHTS = {
|
|
1789
|
+
readsPerEdit: 0.14,
|
|
1790
|
+
rewriteRatio: 0.11,
|
|
1791
|
+
cacheHitRate: 0.11,
|
|
1792
|
+
taskCompletion: 0.1,
|
|
1793
|
+
retryDensity: 0.07,
|
|
1794
|
+
toolDiversity: 0.06,
|
|
1795
|
+
tokensPerEdit: 0.11
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
// src/commands/config-validate.ts
|
|
1799
|
+
async function runConfigValidate() {
|
|
1800
|
+
const config2 = loadConfig();
|
|
1801
|
+
const hasConfig = Object.keys(config2).length > 0;
|
|
1802
|
+
console.log("");
|
|
1803
|
+
if (!hasConfig) {
|
|
1804
|
+
console.log(chalk5.dim(" No .inspecto.json found in current directory. Showing all defaults.\n"));
|
|
1805
|
+
} else {
|
|
1806
|
+
console.log(chalk5.bold(" .inspecto.json") + chalk5.dim(" \u2014 effective configuration\n"));
|
|
1807
|
+
}
|
|
1808
|
+
const thresholdTable = new Table4({
|
|
1809
|
+
head: ["Metric", "healthy", "warning", "Source"].map((h) => chalk5.dim(h)),
|
|
1810
|
+
style: { head: [], border: [], "padding-left": 2, "padding-right": 2 },
|
|
1811
|
+
chars: {
|
|
1812
|
+
top: "\u2500",
|
|
1813
|
+
"top-mid": "\u2500",
|
|
1814
|
+
"top-left": " ",
|
|
1815
|
+
"top-right": "",
|
|
1816
|
+
bottom: "\u2500",
|
|
1817
|
+
"bottom-mid": "\u2500",
|
|
1818
|
+
"bottom-left": " ",
|
|
1819
|
+
"bottom-right": "",
|
|
1820
|
+
left: " ",
|
|
1821
|
+
"left-mid": " ",
|
|
1822
|
+
mid: "\u2500",
|
|
1823
|
+
"mid-mid": "\u2500",
|
|
1824
|
+
right: "",
|
|
1825
|
+
"right-mid": "",
|
|
1826
|
+
middle: " "
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
const thresholdEntries = [
|
|
1830
|
+
["readsPerEdit", "reads-per-edit"],
|
|
1831
|
+
["rewriteRatio", "rewrite-ratio"],
|
|
1832
|
+
["cacheHitRate", "cache-hit-rate"],
|
|
1833
|
+
["taskCompletion", "task-completion"],
|
|
1834
|
+
["retryDensity", "retry-density"],
|
|
1835
|
+
["toolDiversity", "tool-diversity"],
|
|
1836
|
+
["tokensPerEdit", "tokens-per-edit"],
|
|
1837
|
+
["sessionCost", "session-cost"]
|
|
1838
|
+
];
|
|
1839
|
+
for (const [key, label] of thresholdEntries) {
|
|
1840
|
+
const override = config2.thresholds?.[key];
|
|
1841
|
+
const def = DEFAULT_THRESHOLDS[key];
|
|
1842
|
+
const effective = { ...def, ...override };
|
|
1843
|
+
const source = override ? chalk5.green("config") : chalk5.dim("default");
|
|
1844
|
+
thresholdTable.push([label, String(effective.healthy), String(effective.warning), source]);
|
|
1845
|
+
}
|
|
1846
|
+
console.log(chalk5.bold(" Thresholds"));
|
|
1847
|
+
console.log(thresholdTable.toString());
|
|
1848
|
+
console.log("");
|
|
1849
|
+
const weightTable = new Table4({
|
|
1850
|
+
head: ["Metric", "Weight", "Source"].map((h) => chalk5.dim(h)),
|
|
1851
|
+
style: { head: [], border: [], "padding-left": 2, "padding-right": 2 },
|
|
1852
|
+
chars: {
|
|
1853
|
+
top: "\u2500",
|
|
1854
|
+
"top-mid": "\u2500",
|
|
1855
|
+
"top-left": " ",
|
|
1856
|
+
"top-right": "",
|
|
1857
|
+
bottom: "\u2500",
|
|
1858
|
+
"bottom-mid": "\u2500",
|
|
1859
|
+
"bottom-left": " ",
|
|
1860
|
+
"bottom-right": "",
|
|
1861
|
+
left: " ",
|
|
1862
|
+
"left-mid": " ",
|
|
1863
|
+
mid: "\u2500",
|
|
1864
|
+
"mid-mid": "\u2500",
|
|
1865
|
+
right: "",
|
|
1866
|
+
"right-mid": "",
|
|
1867
|
+
middle: " "
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
const weightEntries = [
|
|
1871
|
+
["readsPerEdit", "reads-per-edit"],
|
|
1872
|
+
["rewriteRatio", "rewrite-ratio"],
|
|
1873
|
+
["cacheHitRate", "cache-hit-rate"],
|
|
1874
|
+
["taskCompletion", "task-completion"],
|
|
1875
|
+
["retryDensity", "retry-density"],
|
|
1876
|
+
["toolDiversity", "tool-diversity"],
|
|
1877
|
+
["tokensPerEdit", "tokens-per-edit"]
|
|
1878
|
+
];
|
|
1879
|
+
let configuredWeightSum = 0;
|
|
1880
|
+
for (const [key] of weightEntries) {
|
|
1881
|
+
configuredWeightSum += config2.weights?.[key] ?? DEFAULT_WEIGHTS[key];
|
|
1882
|
+
}
|
|
1883
|
+
for (const [key, label] of weightEntries) {
|
|
1884
|
+
const override = config2.weights?.[key];
|
|
1885
|
+
const effective = override ?? DEFAULT_WEIGHTS[key];
|
|
1886
|
+
const source = override !== void 0 ? chalk5.green("config") : chalk5.dim("default");
|
|
1887
|
+
weightTable.push([label, effective.toFixed(2), source]);
|
|
1888
|
+
}
|
|
1889
|
+
console.log(chalk5.bold(" Weights") + chalk5.dim(` (configurable sum: ${configuredWeightSum.toFixed(2)})`));
|
|
1890
|
+
console.log(weightTable.toString());
|
|
1891
|
+
console.log("");
|
|
1892
|
+
if (config2.dataDir || config2.defaultProject) {
|
|
1893
|
+
console.log(chalk5.bold(" Other settings"));
|
|
1894
|
+
if (config2.dataDir) {
|
|
1895
|
+
console.log(` ${chalk5.dim("dataDir")} ${config2.dataDir} ${chalk5.green("(config)")}`);
|
|
1896
|
+
}
|
|
1897
|
+
if (config2.defaultProject) {
|
|
1898
|
+
console.log(` ${chalk5.dim("defaultProject")} ${config2.defaultProject} ${chalk5.green("(config)")}`);
|
|
1899
|
+
}
|
|
1900
|
+
console.log("");
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1375
1904
|
// src/index.ts
|
|
1376
1905
|
var program = new Command();
|
|
1377
1906
|
program.name("inspecto").description("Claude Code session quality analyzer \u2014 grade sessions, detect regressions, catch cache bugs").version(VERSION);
|
|
1378
|
-
program.command("audit", { isDefault: true }).description("Grade the most recent Claude Code session").option("--json", "Output as JSON").option("--verbose", "Show per-message breakdown").option("--data-dir <path>", "Custom Claude data directory").option("--project <name>", "Filter to a specific project").action(async (options) => {
|
|
1907
|
+
program.command("audit", { isDefault: true }).description("Grade the most recent Claude Code session").option("--json", "Output as JSON").option("--format <format>", "Output format: json, csv").option("--verbose", "Show per-message breakdown").option("--data-dir <path>", "Custom Claude data directory").option("--project <name>", "Filter to a specific project").option("--no-fail", "Always exit 0, even for D/F grades").action(async (options) => {
|
|
1379
1908
|
try {
|
|
1380
1909
|
await runAudit(options);
|
|
1381
1910
|
} catch (error) {
|
|
1382
1911
|
handleError(error);
|
|
1383
1912
|
}
|
|
1384
1913
|
});
|
|
1385
|
-
program.command("trend").description("Analyze quality trends and detect regressions over time").option("--since <duration>", "Time range: 7d, 14d, 30d", "7d").option("--json", "Output as JSON").option("--data-dir <path>", "Custom Claude data directory").option("--project <name>", "Filter to a specific project").action(async (options) => {
|
|
1914
|
+
program.command("trend").description("Analyze quality trends and detect regressions over time").option("--since <duration>", "Time range: 7d, 14d, 30d", "7d").option("--json", "Output as JSON").option("--format <format>", "Output format: json, csv").option("--data-dir <path>", "Custom Claude data directory").option("--project <name>", "Filter to a specific project").option("--no-fail", "Always exit 0, even on regressions").action(async (options) => {
|
|
1386
1915
|
try {
|
|
1387
1916
|
await runTrend(options);
|
|
1388
1917
|
} catch (error) {
|
|
1389
1918
|
handleError(error);
|
|
1390
1919
|
}
|
|
1391
1920
|
});
|
|
1392
|
-
program.command("cache-check").description("Detect prompt cache bugs that inflate token costs").option("--since <duration>", "Time range: 7d, 14d, 30d", "7d").option("--json", "Output as JSON").option("--data-dir <path>", "Custom Claude data directory").action(async (options) => {
|
|
1921
|
+
program.command("cache-check").description("Detect prompt cache bugs that inflate token costs").option("--since <duration>", "Time range: 7d, 14d, 30d", "7d").option("--json", "Output as JSON").option("--data-dir <path>", "Custom Claude data directory").option("--no-fail", "Always exit 0, even when anomalies are detected").action(async (options) => {
|
|
1393
1922
|
try {
|
|
1394
1923
|
await runCacheCheck(options);
|
|
1395
1924
|
} catch (error) {
|
|
1396
1925
|
handleError(error);
|
|
1397
1926
|
}
|
|
1398
1927
|
});
|
|
1399
|
-
program.command("compare").description("Compare quality metrics across projects").requiredOption("--projects <names>", "Comma-separated project names").option("--json", "Output as JSON").option("--data-dir <path>", "Custom Claude data directory").option("--since <duration>", "Time range: 7d, 14d, 30d").action(async (options) => {
|
|
1928
|
+
program.command("compare").description("Compare quality metrics across projects").requiredOption("--projects <names>", "Comma-separated project names").option("--json", "Output as JSON").option("--data-dir <path>", "Custom Claude data directory").option("--since <duration>", "Time range: 7d, 14d, 30d").option("--no-fail", "No-op (compare always exits 0)").action(async (options) => {
|
|
1400
1929
|
try {
|
|
1401
1930
|
await runCompare(options);
|
|
1402
1931
|
} catch (error) {
|
|
1403
1932
|
handleError(error);
|
|
1404
1933
|
}
|
|
1405
1934
|
});
|
|
1935
|
+
program.command("list").description("List discovered projects and sessions").option("--sessions", "Show 20 most recent sessions instead of projects").option("--project <name>", "Filter to sessions for a specific project").option("--data-dir <path>", "Custom Claude data directory").action(async (options) => {
|
|
1936
|
+
try {
|
|
1937
|
+
await runList(options);
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
handleError(error);
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
var config = program.command("config").description("Manage inspecto configuration");
|
|
1943
|
+
config.command("validate").description("Show effective configuration merged from .inspecto.json and defaults").action(async () => {
|
|
1944
|
+
try {
|
|
1945
|
+
await runConfigValidate();
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
handleError(error);
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1406
1950
|
var cache = program.command("cache").description("Manage the inspecto grade cache");
|
|
1407
1951
|
cache.command("clear").description("Delete the grade cache file (~/.claude/inspecto-cache.db)").action(async () => {
|
|
1408
1952
|
try {
|