inspecto 1.0.6 → 1.0.8

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 CHANGED
@@ -23,6 +23,9 @@ The others answer *"how much did I spend?"*
23
23
 
24
24
  `inspecto` answers: **"Is Claude Code getting worse for me — and can I prove it?"**
25
25
 
26
+ <img width="427" height="338" alt="Screenshot 2026-04-11 at 6 00 37 PM" src="https://github.com/user-attachments/assets/81777511-dd45-4ae0-8382-8e008dd98a7a" />
27
+
28
+
26
29
  ---
27
30
 
28
31
  ## Install
package/dist/index.js CHANGED
@@ -32,34 +32,46 @@ Expected: ${projectsDir}`
32
32
  (dir) => dir.toLowerCase().includes(options.project.toLowerCase())
33
33
  );
34
34
  }
35
- const sessions = [];
36
- for (const projectDir of projectDirs) {
37
- if (projectDir.startsWith(".")) continue;
38
- const fullProjectDir = join2(projectsDir, projectDir);
39
- let entries;
40
- try {
41
- entries = await readdir(fullProjectDir);
42
- } catch {
43
- continue;
44
- }
45
- for (const entry of entries) {
46
- if (extname(entry) !== ".jsonl") continue;
47
- const filePath = join2(fullProjectDir, entry);
48
- const sessionId = basename(entry, ".jsonl");
35
+ const projectResults = await Promise.all(
36
+ projectDirs.filter((dir) => !dir.startsWith(".")).map(async (projectDir) => {
37
+ const fullProjectDir = join2(projectsDir, projectDir);
38
+ let entries;
49
39
  try {
50
- const fileStat = await stat(filePath);
51
- if (options?.since && fileStat.mtime < options.since) continue;
52
- sessions.push({
53
- path: filePath,
54
- sessionId,
55
- projectSlug: projectDir,
56
- mtime: fileStat.mtime
57
- });
40
+ entries = await readdir(fullProjectDir);
58
41
  } catch {
59
- continue;
42
+ return [];
60
43
  }
61
- }
62
- }
44
+ const fileResults = await Promise.all(
45
+ entries.filter((entry) => extname(entry) === ".jsonl").map(async (entry) => {
46
+ const filePath = join2(fullProjectDir, entry);
47
+ const sessionId = basename(entry, ".jsonl");
48
+ try {
49
+ const fileStat = await stat(filePath);
50
+ if (options?.since && fileStat.mtime < options.since) return null;
51
+ let subagentPaths;
52
+ try {
53
+ const subagentDir = join2(fullProjectDir, sessionId, "subagents");
54
+ const agentFiles = await readdir(subagentDir);
55
+ const paths = agentFiles.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl")).map((f) => join2(subagentDir, f));
56
+ if (paths.length > 0) subagentPaths = paths;
57
+ } catch {
58
+ }
59
+ return {
60
+ path: filePath,
61
+ sessionId,
62
+ projectSlug: projectDir,
63
+ mtime: fileStat.mtime,
64
+ subagentPaths
65
+ };
66
+ } catch {
67
+ return null;
68
+ }
69
+ })
70
+ );
71
+ return fileResults.filter((f) => f !== null);
72
+ })
73
+ );
74
+ const sessions = projectResults.flat();
63
75
  sessions.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
64
76
  return sessions;
65
77
  }
@@ -93,40 +105,53 @@ async function* readJsonl(filePath) {
93
105
  }
94
106
 
95
107
  // src/parser/session-builder.ts
96
- async function buildSession(records, sessionId, projectSlug) {
97
- const assistantChunks = /* @__PURE__ */ new Map();
108
+ import { basename as basename2 } from "path";
109
+ async function buildSession(records, sessionId, projectSlug, subagentPaths) {
98
110
  const turns = [];
99
111
  let cwd = "";
100
112
  let gitBranch = null;
101
113
  let model = "";
102
114
  let firstTimestamp = "";
103
115
  let lastTimestamp = "";
104
- for await (const record of records) {
105
- if (isSkippable(record.type)) continue;
106
- if (record.type === "user") {
107
- const userRecord = record;
108
- handleUserRecord(userRecord, turns);
109
- captureMetadata(userRecord);
110
- } else if (record.type === "assistant") {
111
- const assistantRecord = record;
112
- if (assistantRecord.message.model === "<synthetic>") continue;
113
- if (assistantRecord.error) continue;
114
- handleAssistantChunk(assistantRecord, assistantChunks);
115
- captureMetadata(assistantRecord);
116
+ async function processRecords(stream, agentId) {
117
+ const assistantChunks = /* @__PURE__ */ new Map();
118
+ for await (const record of stream) {
119
+ if (isSkippable(record.type)) continue;
120
+ if (record.type === "user") {
121
+ const userRecord = record;
122
+ handleUserRecord(userRecord, agentId);
123
+ captureMetadata(userRecord);
124
+ } else if (record.type === "assistant") {
125
+ const assistantRecord = record;
126
+ if (assistantRecord.message.model === "<synthetic>") continue;
127
+ if (assistantRecord.error) continue;
128
+ handleAssistantChunk(assistantRecord, assistantChunks);
129
+ captureMetadata(assistantRecord);
130
+ }
131
+ }
132
+ for (const [, acc] of assistantChunks) {
133
+ turns.push({
134
+ role: "assistant",
135
+ content: acc.content,
136
+ usage: acc.usage,
137
+ complete: acc.complete,
138
+ timestamp: acc.timestamp,
139
+ isHumanTurn: false,
140
+ model: acc.model,
141
+ agentId
142
+ });
116
143
  }
117
144
  }
118
- for (const [, acc] of assistantChunks) {
119
- turns.push({
120
- role: "assistant",
121
- content: acc.content,
122
- usage: acc.usage,
123
- complete: acc.complete,
124
- timestamp: acc.timestamp,
125
- isHumanTurn: false,
126
- model: acc.model
127
- });
145
+ await processRecords(records, void 0);
146
+ for (const agentPath of subagentPaths ?? []) {
147
+ const agentId = basename2(agentPath, ".jsonl");
148
+ await processRecords(readJsonl(agentPath), agentId);
128
149
  }
129
150
  turns.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
151
+ const subagentIds = new Set(
152
+ turns.filter((t) => t.agentId !== void 0).map((t) => t.agentId)
153
+ );
154
+ const subagentTurnCount = turns.filter((t) => t.agentId !== void 0).length;
130
155
  return {
131
156
  id: sessionId,
132
157
  projectSlug,
@@ -136,7 +161,9 @@ async function buildSession(records, sessionId, projectSlug) {
136
161
  endTime: lastTimestamp,
137
162
  cwd,
138
163
  gitBranch,
139
- durationMs: firstTimestamp && lastTimestamp ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime() : 0
164
+ durationMs: firstTimestamp && lastTimestamp ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime() : 0,
165
+ subagentCount: subagentIds.size,
166
+ subagentTurnCount
140
167
  };
141
168
  function captureMetadata(record) {
142
169
  if (!firstTimestamp && record.timestamp) {
@@ -158,16 +185,17 @@ async function buildSession(records, sessionId, projectSlug) {
158
185
  }
159
186
  }
160
187
  }
161
- function handleUserRecord(record, turns2) {
188
+ function handleUserRecord(record, agentId) {
162
189
  const content = record.message.content;
163
190
  const isHumanTurn = typeof content === "string" && !record.isMeta;
164
- turns2.push({
191
+ turns.push({
165
192
  role: "user",
166
193
  content: normalizeContent(content),
167
194
  usage: null,
168
195
  complete: true,
169
196
  timestamp: record.timestamp,
170
- isHumanTurn
197
+ isHumanTurn,
198
+ agentId
171
199
  });
172
200
  }
173
201
  function handleAssistantChunk(record, chunks) {
@@ -524,6 +552,48 @@ function computeTokensPerEdit(session) {
524
552
  };
525
553
  }
526
554
 
555
+ // src/metrics/subagent-overhead.ts
556
+ function computeSubagentOverhead(session) {
557
+ if (session.subagentCount === 0) {
558
+ return {
559
+ name: "subagent-overhead",
560
+ value: null,
561
+ status: "healthy",
562
+ label: "N/A",
563
+ detail: "No subagents in this session"
564
+ };
565
+ }
566
+ let mainTokens = 0;
567
+ let subagentTokens = 0;
568
+ for (const turn of session.turns) {
569
+ if (turn.role !== "assistant" || !turn.usage) continue;
570
+ const out = turn.usage.output_tokens ?? 0;
571
+ if (turn.agentId === void 0) {
572
+ mainTokens += out;
573
+ } else {
574
+ subagentTokens += out;
575
+ }
576
+ }
577
+ const total = mainTokens + subagentTokens;
578
+ if (total === 0) {
579
+ return {
580
+ name: "subagent-overhead",
581
+ value: null,
582
+ status: "healthy",
583
+ label: "N/A",
584
+ detail: "No output tokens recorded"
585
+ };
586
+ }
587
+ const ratio = mainTokens / total;
588
+ const status = ratio < 0.6 ? "healthy" : ratio < 0.8 ? "warning" : "critical";
589
+ return {
590
+ name: "subagent-overhead",
591
+ value: Math.round(ratio * 100) / 100,
592
+ status,
593
+ label: `${Math.round(ratio * 100)}% main`
594
+ };
595
+ }
596
+
527
597
  // src/metrics/grader.ts
528
598
  var METRIC_WEIGHTS = [
529
599
  {
@@ -558,7 +628,7 @@ var METRIC_WEIGHTS = [
558
628
  },
559
629
  {
560
630
  compute: computeToolDiversity,
561
- weight: 0.1,
631
+ weight: 0.05,
562
632
  // 0 → 0, 0.4 → 50, 0.6+ → 100
563
633
  score: (v) => clamp(v / 0.6 * 100, 0, 100)
564
634
  },
@@ -567,6 +637,12 @@ var METRIC_WEIGHTS = [
567
637
  weight: 0.15,
568
638
  // 5000 → 100, 10000 → 50, 15000+ → 0 (inverted)
569
639
  score: (v) => clamp((1 - (v - 5e3) / 1e4) * 100, 0, 100)
640
+ },
641
+ {
642
+ compute: computeSubagentOverhead,
643
+ weight: 0.05,
644
+ // main ratio 0 → 100, 0.6 → 100 (threshold), 0.8 → 50, 1.0 → 0 (inverted)
645
+ score: (v) => clamp((1 - v) / 0.4 * 100, 0, 100)
570
646
  }
571
647
  ];
572
648
  var GRADE_THRESHOLDS = [
@@ -690,18 +766,21 @@ var METRIC_DISPLAY_NAMES = {
690
766
  "task-completion": "Task completion",
691
767
  "retry-density": "Retry density",
692
768
  "tool-diversity": "Tool diversity",
693
- "tokens-per-edit": "Tokens/useful-edit"
769
+ "tokens-per-edit": "Tokens/useful-edit",
770
+ "subagent-overhead": "Subagent delegation"
694
771
  };
695
772
  function renderAuditReport(session, grade) {
696
773
  const lines = [];
697
774
  lines.push("");
698
775
  lines.push(chalk.bold(" inspecto v1.0.0") + chalk.dim(" \u2014 Claude Code Session Quality Analyzer"));
699
776
  lines.push("");
777
+ const agentInfo = session.subagentCount > 0 ? `${session.subagentCount} subagents | ${session.turns.length} turns` : `${session.turns.length} turns`;
700
778
  const sessionInfo = [
701
779
  `Session: ${chalk.cyan(shortSessionId(session.id))}`,
702
780
  projectNameFromSlug(session.projectSlug),
703
781
  formatDuration(session.durationMs),
704
- session.model
782
+ session.model,
783
+ agentInfo
705
784
  ].join(chalk.dim(" | "));
706
785
  lines.push(` ${sessionInfo}`);
707
786
  lines.push("");
@@ -905,7 +984,8 @@ async function runAudit(options) {
905
984
  const session = await buildSession(
906
985
  records,
907
986
  sessionFile.sessionId,
908
- sessionFile.projectSlug
987
+ sessionFile.projectSlug,
988
+ sessionFile.subagentPaths
909
989
  );
910
990
  const grade = gradeSession(session);
911
991
  if (options.json) {
@@ -1004,15 +1084,14 @@ async function runTrend(options) {
1004
1084
  console.log(`No sessions found in the last ${duration}.`);
1005
1085
  return;
1006
1086
  }
1007
- const grades = [];
1008
- for (const sf of sessionFiles) {
1009
- try {
1087
+ const settled = await Promise.allSettled(
1088
+ sessionFiles.map(async (sf) => {
1010
1089
  const records = readJsonl(sf.path);
1011
- const session = await buildSession(records, sf.sessionId, sf.projectSlug);
1012
- grades.push(gradeSession(session));
1013
- } catch {
1014
- }
1015
- }
1090
+ const session = await buildSession(records, sf.sessionId, sf.projectSlug, sf.subagentPaths);
1091
+ return gradeSession(session);
1092
+ })
1093
+ );
1094
+ const grades = settled.filter((r) => r.status === "fulfilled").map((r) => r.value);
1016
1095
  if (grades.length === 0) {
1017
1096
  console.log("No valid sessions found to analyze.");
1018
1097
  return;
@@ -1059,15 +1138,14 @@ async function runCacheCheck(options) {
1059
1138
  console.log(`No sessions found in the last ${duration}.`);
1060
1139
  return;
1061
1140
  }
1062
- const results = [];
1063
- for (const sf of sessionFiles) {
1064
- try {
1141
+ const settled = await Promise.allSettled(
1142
+ sessionFiles.map(async (sf) => {
1065
1143
  const records = readJsonl(sf.path);
1066
- const session = await buildSession(records, sf.sessionId, sf.projectSlug);
1067
- results.push(checkCacheAnomaly(session));
1068
- } catch {
1069
- }
1070
- }
1144
+ const session = await buildSession(records, sf.sessionId, sf.projectSlug, sf.subagentPaths);
1145
+ return checkCacheAnomaly(session);
1146
+ })
1147
+ );
1148
+ const results = settled.filter((r) => r.status === "fulfilled").map((r) => r.value);
1071
1149
  if (results.length === 0) {
1072
1150
  console.log("No valid sessions found to analyze.");
1073
1151
  return;
@@ -1085,38 +1163,41 @@ import Table2 from "cli-table3";
1085
1163
  async function runCompare(options) {
1086
1164
  const projectNames = options.projects.split(",").map((p) => p.trim());
1087
1165
  const summaries = [];
1088
- for (const projectFilter of projectNames) {
1089
- const sessionFiles = await scanSessions({
1090
- dataDir: options.dataDir,
1091
- project: projectFilter
1092
- });
1093
- if (sessionFiles.length === 0) continue;
1094
- const grades = [];
1095
- for (const sf of sessionFiles) {
1096
- try {
1097
- const records = readJsonl(sf.path);
1098
- const session = await buildSession(records, sf.sessionId, sf.projectSlug);
1099
- grades.push(gradeSession(session));
1100
- } catch {
1101
- continue;
1102
- }
1103
- }
1104
- if (grades.length === 0) continue;
1105
- const avgScore = grades.reduce((s, g) => s + g.score, 0) / grades.length;
1106
- const metricAvgs = /* @__PURE__ */ new Map();
1107
- for (const metric of grades[0].metrics) {
1108
- const values = grades.map((g) => g.metrics.find((m) => m.name === metric.name)?.value).filter((v) => v !== null);
1109
- if (values.length > 0) {
1110
- metricAvgs.set(metric.name, values.reduce((a, b) => a + b, 0) / values.length);
1166
+ const projectSummaries = await Promise.all(
1167
+ projectNames.map(async (projectFilter) => {
1168
+ const sessionFiles = await scanSessions({
1169
+ dataDir: options.dataDir,
1170
+ project: projectFilter
1171
+ });
1172
+ if (sessionFiles.length === 0) return null;
1173
+ const settled = await Promise.allSettled(
1174
+ sessionFiles.map(async (sf) => {
1175
+ const records = readJsonl(sf.path);
1176
+ const session = await buildSession(records, sf.sessionId, sf.projectSlug, sf.subagentPaths);
1177
+ return gradeSession(session);
1178
+ })
1179
+ );
1180
+ const grades = settled.filter((r) => r.status === "fulfilled").map((r) => r.value);
1181
+ if (grades.length === 0) return null;
1182
+ const avgScore = grades.reduce((s, g) => s + g.score, 0) / grades.length;
1183
+ const metricAvgs = /* @__PURE__ */ new Map();
1184
+ for (const metric of grades[0].metrics) {
1185
+ const values = grades.map((g) => g.metrics.find((m) => m.name === metric.name)?.value).filter((v) => v !== null);
1186
+ if (values.length > 0) {
1187
+ metricAvgs.set(metric.name, values.reduce((a, b) => a + b, 0) / values.length);
1188
+ }
1111
1189
  }
1112
- }
1113
- summaries.push({
1114
- name: projectFilter,
1115
- sessionCount: grades.length,
1116
- avgGrade: Math.round(avgScore),
1117
- avgLetter: getLetterGrade(avgScore),
1118
- metrics: metricAvgs
1119
- });
1190
+ return {
1191
+ name: projectFilter,
1192
+ sessionCount: grades.length,
1193
+ avgGrade: Math.round(avgScore),
1194
+ avgLetter: getLetterGrade(avgScore),
1195
+ metrics: metricAvgs
1196
+ };
1197
+ })
1198
+ );
1199
+ for (const s of projectSummaries) {
1200
+ if (s !== null) summaries.push(s);
1120
1201
  }
1121
1202
  if (summaries.length === 0) {
1122
1203
  console.log("No matching projects found.");
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/parser/project-scanner.ts","../src/utils/paths.ts","../src/parser/jsonl-reader.ts","../src/parser/session-builder.ts","../src/metrics/reads-per-edit.ts","../src/metrics/rewrite-ratio.ts","../src/metrics/cache-hit-rate.ts","../src/metrics/task-completion.ts","../src/utils/levenshtein.ts","../src/metrics/retry-density.ts","../src/metrics/tool-diversity.ts","../src/metrics/tokens-per-edit.ts","../src/metrics/grader.ts","../src/reporter/terminal.ts","../src/utils/format.ts","../src/reporter/tips.ts","../src/reporter/json-reporter.ts","../src/commands/audit.ts","../src/anomaly/baseline.ts","../src/anomaly/regression-detector.ts","../src/utils/duration.ts","../src/commands/trend.ts","../src/anomaly/cache-anomaly.ts","../src/commands/cache-check.ts","../src/commands/compare.ts"],"sourcesContent":["/**\n * inspecto — Claude Code Session Quality Analyzer\n *\n * Grade sessions, detect regressions, catch cache bugs.\n * All from the JSONL logs Claude Code already writes.\n */\n\nimport { Command } from \"commander\";\nimport { runAudit } from \"./commands/audit.js\";\nimport { runTrend } from \"./commands/trend.js\";\nimport { runCacheCheck } from \"./commands/cache-check.js\";\nimport { runCompare } from \"./commands/compare.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"inspecto\")\n .description(\"Claude Code session quality analyzer — grade sessions, detect regressions, catch cache bugs\")\n .version(\"1.0.0\");\n\nprogram\n .command(\"audit\", { isDefault: true })\n .description(\"Grade the most recent Claude Code session\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verbose\", \"Show per-message breakdown\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .option(\"--project <name>\", \"Filter to a specific project\")\n .action(async (options) => {\n try {\n await runAudit(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"trend\")\n .description(\"Analyze quality trends and detect regressions over time\")\n .option(\"--since <duration>\", \"Time range: 7d, 14d, 30d\", \"7d\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .option(\"--project <name>\", \"Filter to a specific project\")\n .action(async (options) => {\n try {\n await runTrend(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"cache-check\")\n .description(\"Detect prompt cache bugs that inflate token costs\")\n .option(\"--since <duration>\", \"Time range: 7d, 14d, 30d\", \"7d\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .action(async (options) => {\n try {\n await runCacheCheck(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compare\")\n .description(\"Compare quality metrics across projects\")\n .requiredOption(\"--projects <names>\", \"Comma-separated project names\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .option(\"--since <duration>\", \"Time range: 7d, 14d, 30d\")\n .action(async (options) => {\n try {\n await runCompare(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nfunction handleError(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`\\nError: ${message}\\n`);\n process.exit(1);\n}\n\nprogram.parse();\n","/**\n * Discovers Claude Code session files under ~/.claude/projects/.\n *\n * Session files are at: ~/.claude/projects/{project-slug}/{sessionId}.jsonl\n * Subagent files are at: ~/.claude/projects/{project-slug}/{sessionId}/subagents/agent-*.jsonl\n */\n\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join, basename, extname } from \"node:path\";\nimport { getClaudeDir } from \"../utils/paths.js\";\nimport type { SessionFile } from \"./types.js\";\n\n/**\n * Scan ~/.claude/projects/ for all main session JSONL files.\n * Returns files sorted by modification time (most recent first).\n */\nexport async function scanSessions(options?: {\n dataDir?: string;\n project?: string;\n since?: Date;\n}): Promise<SessionFile[]> {\n const claudeDir = options?.dataDir ?? getClaudeDir();\n const projectsDir = join(claudeDir, \"projects\");\n\n let projectDirs: string[];\n try {\n projectDirs = await readdir(projectsDir);\n } catch {\n throw new Error(\n \"Claude Code data directory not found. \" +\n \"Make sure Claude Code is installed and has been used at least once.\\n\" +\n `Expected: ${projectsDir}`,\n );\n }\n\n // Filter to specific project if requested\n if (options?.project) {\n projectDirs = projectDirs.filter((dir) =>\n dir.toLowerCase().includes(options.project!.toLowerCase()),\n );\n }\n\n const sessions: SessionFile[] = [];\n\n for (const projectDir of projectDirs) {\n // Skip hidden directories\n if (projectDir.startsWith(\".\")) continue;\n\n const fullProjectDir = join(projectsDir, projectDir);\n let entries: string[];\n try {\n entries = await readdir(fullProjectDir);\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (extname(entry) !== \".jsonl\") continue;\n\n const filePath = join(fullProjectDir, entry);\n const sessionId = basename(entry, \".jsonl\");\n\n try {\n const fileStat = await stat(filePath);\n\n // Filter by date if requested\n if (options?.since && fileStat.mtime < options.since) continue;\n\n sessions.push({\n path: filePath,\n sessionId,\n projectSlug: projectDir,\n mtime: fileStat.mtime,\n });\n } catch {\n continue;\n }\n }\n }\n\n // Sort most recent first\n sessions.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n return sessions;\n}\n\n/**\n * Get the most recent session file, optionally filtered by project.\n */\nexport async function getMostRecentSession(options?: {\n dataDir?: string;\n project?: string;\n}): Promise<SessionFile> {\n const sessions = await scanSessions(options);\n if (sessions.length === 0) {\n throw new Error(\n \"No Claude Code sessions found. \" +\n \"Use Claude Code in a project first to generate session data.\",\n );\n }\n return sessions[0];\n}\n","/**\n * Cross-platform path resolution for Claude Code data directories.\n */\n\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/**\n * Returns the Claude Code data directory.\n * macOS/Linux: ~/.claude\n * Windows: %USERPROFILE%\\.claude\n */\nexport function getClaudeDir(): string {\n return join(homedir(), \".claude\");\n}\n","/**\n * Streaming JSONL reader using Node's readline + createReadStream.\n * Processes files line-by-line to handle 100MB+ session files without\n * loading them into memory.\n */\n\nimport { createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport type { RawRecord } from \"./types.js\";\n\n/**\n * Stream-reads a JSONL file, yielding one parsed record per line.\n * Malformed lines are silently skipped (common near session end during crashes).\n */\nexport async function* readJsonl(filePath: string): AsyncGenerator<RawRecord> {\n const stream = createReadStream(filePath, { encoding: \"utf-8\" });\n const rl = createInterface({ input: stream, crlfDelay: Infinity });\n\n for await (const line of rl) {\n const trimmed = line.trim();\n if (trimmed.length === 0) continue;\n\n try {\n const record = JSON.parse(trimmed) as RawRecord;\n if (record && typeof record === \"object\" && \"type\" in record) {\n yield record;\n }\n } catch {\n // Skip malformed lines — common at session boundaries\n }\n }\n}\n","/**\n * Builds a Session from raw JSONL records.\n *\n * Handles the core complexity of Claude Code's streaming format:\n * - Assistant turns are split across multiple JSONL records sharing the same\n * `message.id`. Content blocks from each chunk are merged into one turn.\n * - Only the final chunk (stop_reason != null) has real output_tokens.\n * - Synthetic records (model: \"<synthetic>\") and errored turns are excluded.\n */\n\nimport type {\n AssistantRecord,\n ContentBlock,\n MergedTurn,\n RawRecord,\n Session,\n SKIP_TYPES,\n UsageData,\n UserRecord,\n} from \"./types.js\";\n\ninterface AssistantAccumulator {\n content: ContentBlock[];\n usage: UsageData | null;\n complete: boolean;\n timestamp: string;\n model: string;\n}\n\n/**\n * Build a processed Session from an async stream of raw records.\n * @param records - AsyncIterable of raw JSONL records (from readJsonl)\n * @param sessionId - The session ID (from filename)\n * @param projectSlug - The project slug (from parent directory name)\n */\nexport async function buildSession(\n records: AsyncIterable<RawRecord>,\n sessionId: string,\n projectSlug: string,\n): Promise<Session> {\n const assistantChunks = new Map<string, AssistantAccumulator>();\n const turns: MergedTurn[] = [];\n\n let cwd = \"\";\n let gitBranch: string | null = null;\n let model = \"\";\n let firstTimestamp = \"\";\n let lastTimestamp = \"\";\n\n for await (const record of records) {\n // Skip non-conversation record types\n if (isSkippable(record.type)) continue;\n\n if (record.type === \"user\") {\n const userRecord = record as UserRecord;\n handleUserRecord(userRecord, turns);\n captureMetadata(userRecord);\n } else if (record.type === \"assistant\") {\n const assistantRecord = record as AssistantRecord;\n\n // Skip synthetic context-management records\n if (assistantRecord.message.model === \"<synthetic>\") continue;\n // Skip errored API responses\n if (assistantRecord.error) continue;\n\n handleAssistantChunk(assistantRecord, assistantChunks);\n captureMetadata(assistantRecord);\n }\n }\n\n // Flush all accumulated assistant chunks into turns\n for (const [, acc] of assistantChunks) {\n turns.push({\n role: \"assistant\",\n content: acc.content,\n usage: acc.usage,\n complete: acc.complete,\n timestamp: acc.timestamp,\n isHumanTurn: false,\n model: acc.model,\n });\n }\n\n // Sort all turns by timestamp\n turns.sort((a, b) => a.timestamp.localeCompare(b.timestamp));\n\n return {\n id: sessionId,\n projectSlug,\n model,\n turns,\n startTime: firstTimestamp,\n endTime: lastTimestamp,\n cwd,\n gitBranch,\n durationMs: firstTimestamp && lastTimestamp\n ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime()\n : 0,\n };\n\n // -- Inner helpers --------------------------------------------------------\n\n function captureMetadata(record: UserRecord | AssistantRecord) {\n if (!firstTimestamp && record.timestamp) {\n firstTimestamp = record.timestamp;\n }\n if (record.timestamp) {\n lastTimestamp = record.timestamp;\n }\n if (!cwd && record.cwd) {\n cwd = record.cwd;\n }\n if (gitBranch === null && record.gitBranch) {\n gitBranch = record.gitBranch;\n }\n if (!model && record.type === \"assistant\") {\n const ar = record as AssistantRecord;\n if (ar.message.model && ar.message.model !== \"<synthetic>\") {\n model = ar.message.model;\n }\n }\n }\n\n function handleUserRecord(record: UserRecord, turns: MergedTurn[]) {\n const content = record.message.content;\n const isHumanTurn =\n typeof content === \"string\" && !record.isMeta;\n\n turns.push({\n role: \"user\",\n content: normalizeContent(content),\n usage: null,\n complete: true,\n timestamp: record.timestamp,\n isHumanTurn,\n });\n }\n\n function handleAssistantChunk(\n record: AssistantRecord,\n chunks: Map<string, AssistantAccumulator>,\n ) {\n const messageId = record.message.id;\n let acc = chunks.get(messageId);\n\n if (!acc) {\n acc = {\n content: [],\n usage: null,\n complete: false,\n timestamp: record.timestamp,\n model: record.message.model,\n };\n chunks.set(messageId, acc);\n }\n\n // Append content blocks from this streaming chunk\n for (const block of record.message.content) {\n acc.content.push(block);\n }\n\n // Final chunk has the real usage data\n if (record.message.stop_reason !== null) {\n acc.complete = true;\n acc.usage = record.message.usage;\n }\n }\n}\n\nfunction normalizeContent(content: string | ContentBlock[]): ContentBlock[] {\n if (typeof content === \"string\") {\n return [{ type: \"text\", text: content }];\n }\n return content;\n}\n\nconst SKIPPABLE = new Set([\n \"queue-operation\",\n \"attachment\",\n \"system\",\n \"last-prompt\",\n]);\n\nfunction isSkippable(type: string): boolean {\n return SKIPPABLE.has(type);\n}\n","/**\n * M1: Reads-before-edit ratio.\n *\n * Counts how many Read tool_use events occur before each Write or Edit event.\n * High values mean Claude is reading context before modifying files.\n * The AMD data showed this dropped from 6.6 to 2.0 after March 8.\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nconst EDIT_TOOLS = new Set([\"Write\", \"Edit\", \"NotebookEdit\"]);\nconst READ_TOOL = \"Read\";\n\nexport function computeReadsPerEdit(session: Session): MetricResult {\n let readsSinceLastEdit = 0;\n const ratios: number[] = [];\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n\n if (toolBlock.name === READ_TOOL) {\n readsSinceLastEdit++;\n } else if (EDIT_TOOLS.has(toolBlock.name)) {\n ratios.push(readsSinceLastEdit);\n readsSinceLastEdit = 0;\n }\n }\n }\n\n if (ratios.length === 0) {\n return {\n name: \"reads-per-edit\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No file modifications in this session\",\n };\n }\n\n const average = ratios.reduce((a, b) => a + b, 0) / ratios.length;\n\n return {\n name: \"reads-per-edit\",\n value: round(average),\n status: average >= 4.0 ? \"healthy\" : average >= 2.0 ? \"warning\" : \"critical\",\n label: round(average).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M2: Full-file rewrite ratio.\n *\n * Ratio of Write calls (full file replacement) to total file modifications\n * (Write + Edit). Rising ratio means Claude is rewriting instead of\n * making surgical edits.\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nexport function computeRewriteRatio(session: Session): MetricResult {\n let writes = 0;\n let edits = 0;\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n\n if (toolBlock.name === \"Write\") writes++;\n else if (toolBlock.name === \"Edit\" || toolBlock.name === \"NotebookEdit\") edits++;\n }\n }\n\n const total = writes + edits;\n if (total === 0) {\n return {\n name: \"rewrite-ratio\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No file modifications in this session\",\n };\n }\n\n const ratio = writes / total;\n\n return {\n name: \"rewrite-ratio\",\n value: round(ratio),\n status: ratio <= 0.25 ? \"healthy\" : ratio <= 0.5 ? \"warning\" : \"critical\",\n label: round(ratio).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M3: Cache hit rate.\n *\n * Ratio of cache_read_input_tokens to total input tokens\n * (cache_read + cache_creation). Detects the prompt cache bug\n * that caused 10-20x cost inflation.\n *\n * Note: raw `input_tokens` is always a streaming placeholder (1 or 3).\n * Real input cost = cache_read + cache_creation.\n */\n\nimport type { MetricResult, Session } from \"../parser/types.js\";\n\nexport function computeCacheHitRate(session: Session): MetricResult {\n let totalCacheRead = 0;\n let totalCacheCreation = 0;\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\" || !turn.usage || !turn.complete) continue;\n\n totalCacheRead += turn.usage.cache_read_input_tokens;\n totalCacheCreation += turn.usage.cache_creation_input_tokens;\n }\n\n const totalInput = totalCacheRead + totalCacheCreation;\n if (totalInput === 0) {\n return {\n name: \"cache-hit-rate\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No token usage data available\",\n };\n }\n\n const rate = totalCacheRead / totalInput;\n\n return {\n name: \"cache-hit-rate\",\n value: round(rate),\n status: rate >= 0.5 ? \"healthy\" : rate >= 0.2 ? \"warning\" : \"critical\",\n label: round(rate).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M4: Task completion rate.\n *\n * Detects sessions where Claude says it will do something but doesn't\n * follow through. Looks for intent phrases in assistant text that\n * aren't followed by a tool_use in the next assistant turn.\n */\n\nimport type { MetricResult, Session, MergedTurn, TextBlock, ToolUseBlock } from \"../parser/types.js\";\n\nconst INTENT_PATTERNS = [\n /\\bI'll now\\b/i,\n /\\bLet me\\b/i,\n /\\bI'll update\\b/i,\n /\\bNext,? I'll\\b/i,\n /\\bI'll (?:also |then )?(?:fix|add|create|implement|refactor|modify|change|write|edit|update)\\b/i,\n /\\bI'm going to\\b/i,\n];\n\nexport function computeTaskCompletion(session: Session): MetricResult {\n const assistantTurns = session.turns.filter(\n (t) => t.role === \"assistant\" && t.complete,\n );\n\n let totalIntents = 0;\n let unfulfilledIntents = 0;\n\n for (const turn of assistantTurns) {\n const hasIntent = hasIntentPhrase(turn);\n if (!hasIntent) continue;\n\n totalIntents++;\n\n // An intent is fulfilled if the same merged turn also contains a tool_use.\n // Since streaming chunks are merged, a real action within this turn means\n // Claude followed through. An intent without a tool_use in the same turn\n // is a dangling promise.\n const hasToolUse = turn.content.some((b) => b.type === \"tool_use\");\n if (!hasToolUse) {\n unfulfilledIntents++;\n }\n }\n\n if (totalIntents === 0) {\n return {\n name: \"task-completion\",\n value: 1,\n status: \"healthy\",\n label: \"1.00\",\n detail: \"No intent phrases detected\",\n };\n }\n\n const rate = 1 - unfulfilledIntents / totalIntents;\n\n return {\n name: \"task-completion\",\n value: round(rate),\n status: rate >= 0.9 ? \"healthy\" : rate >= 0.7 ? \"warning\" : \"critical\",\n label: round(rate).toString(),\n };\n}\n\nfunction hasIntentPhrase(turn: MergedTurn): boolean {\n for (const block of turn.content) {\n if (block.type === \"text\") {\n const textBlock = block as TextBlock;\n if (INTENT_PATTERNS.some((p) => p.test(textBlock.text))) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * Levenshtein distance and normalized similarity.\n * Pure implementation — no external dependencies.\n */\n\n/**\n * Compute the Levenshtein edit distance between two strings.\n * Uses a single-row DP approach for O(min(m,n)) space.\n */\nexport function levenshteinDistance(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n // Ensure a is the shorter string for space optimization\n if (a.length > b.length) [a, b] = [b, a];\n\n const aLen = a.length;\n const bLen = b.length;\n const row = new Array<number>(aLen + 1);\n\n for (let i = 0; i <= aLen; i++) row[i] = i;\n\n for (let j = 1; j <= bLen; j++) {\n let prev = row[0];\n row[0] = j;\n\n for (let i = 1; i <= aLen; i++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n const temp = row[i];\n row[i] = Math.min(\n row[i] + 1, // deletion\n row[i - 1] + 1, // insertion\n prev + cost, // substitution\n );\n prev = temp;\n }\n }\n\n return row[aLen];\n}\n\n/**\n * Compute normalized similarity between two strings (0 = different, 1 = identical).\n * Only compares the first `maxLen` characters for performance.\n */\nexport function normalizedSimilarity(\n a: string,\n b: string,\n maxLen = 200,\n): number {\n const aTrunc = a.slice(0, maxLen);\n const bTrunc = b.slice(0, maxLen);\n const maxLength = Math.max(aTrunc.length, bTrunc.length);\n\n if (maxLength === 0) return 1;\n\n const distance = levenshteinDistance(aTrunc, bTrunc);\n return 1 - distance / maxLength;\n}\n","/**\n * M5: Retry density.\n *\n * Measures how often the user sends messages very similar to their\n * previous message — a proxy for \"Claude got it wrong and I'm asking again.\"\n */\n\nimport type { MetricResult, Session, TextBlock } from \"../parser/types.js\";\nimport { normalizedSimilarity } from \"../utils/levenshtein.js\";\n\nexport function computeRetryDensity(session: Session): MetricResult {\n // Extract text from human-authored user turns only\n const humanTexts: string[] = [];\n for (const turn of session.turns) {\n if (!turn.isHumanTurn) continue;\n const text = turn.content\n .filter((b): b is TextBlock => b.type === \"text\")\n .map((b) => b.text)\n .join(\" \");\n if (text.length > 0) humanTexts.push(text);\n }\n\n if (humanTexts.length < 2) {\n return {\n name: \"retry-density\",\n value: 0,\n status: \"healthy\",\n label: \"0.00\",\n detail: \"Not enough user messages to detect retries\",\n };\n }\n\n let retries = 0;\n const pairs = humanTexts.length - 1;\n\n for (let i = 0; i < pairs; i++) {\n const similarity = normalizedSimilarity(humanTexts[i], humanTexts[i + 1]);\n if (similarity > 0.6) {\n retries++;\n }\n }\n\n const density = retries / pairs;\n\n return {\n name: \"retry-density\",\n value: round(density),\n status: density <= 0.1 ? \"healthy\" : density <= 0.25 ? \"warning\" : \"critical\",\n label: round(density).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M6: Tool diversity score.\n *\n * Shannon entropy over the distribution of tool_use events by tool name.\n * Normalized to 0-1. Low diversity means Claude is over-relying on\n * one tool (often Write).\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nexport function computeToolDiversity(session: Session): MetricResult {\n const toolCounts = new Map<string, number>();\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n toolCounts.set(toolBlock.name, (toolCounts.get(toolBlock.name) ?? 0) + 1);\n }\n }\n\n const uniqueTools = toolCounts.size;\n if (uniqueTools <= 1) {\n return {\n name: \"tool-diversity\",\n value: uniqueTools === 0 ? null : 0,\n status: uniqueTools === 0 ? \"healthy\" : \"critical\",\n label: uniqueTools === 0 ? \"N/A\" : \"0.00\",\n detail: uniqueTools === 0\n ? \"No tool usage in this session\"\n : `Only one tool used: ${[...toolCounts.keys()][0]}`,\n };\n }\n\n const totalCalls = [...toolCounts.values()].reduce((a, b) => a + b, 0);\n const maxEntropy = Math.log2(uniqueTools);\n\n let entropy = 0;\n for (const count of toolCounts.values()) {\n const p = count / totalCalls;\n entropy -= p * Math.log2(p);\n }\n\n const normalized = entropy / maxEntropy;\n\n // Build detail showing top tools\n const sorted = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);\n const topTool = sorted[0];\n const topPercent = Math.round((topTool[1] / totalCalls) * 100);\n const detail = `Most used: ${topTool[0]} (${topPercent}%)`;\n\n return {\n name: \"tool-diversity\",\n value: round(normalized),\n status: normalized >= 0.6 ? \"healthy\" : normalized >= 0.4 ? \"warning\" : \"critical\",\n label: round(normalized).toString(),\n detail,\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M7: Tokens per useful edit.\n *\n * Total output tokens consumed divided by number of file modification\n * operations (Write + Edit). Rising ratio means Claude is burning more\n * tokens per productive action.\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nconst EDIT_TOOLS = new Set([\"Write\", \"Edit\", \"NotebookEdit\"]);\n\nexport function computeTokensPerEdit(session: Session): MetricResult {\n let totalOutputTokens = 0;\n let editCount = 0;\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n // Count tokens from completed turns only\n if (turn.complete && turn.usage) {\n totalOutputTokens += turn.usage.output_tokens;\n }\n\n // Count edit operations\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n if (EDIT_TOOLS.has(toolBlock.name)) editCount++;\n }\n }\n\n if (editCount === 0) {\n return {\n name: \"tokens-per-edit\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No file modifications in this session\",\n };\n }\n\n const ratio = totalOutputTokens / editCount;\n\n return {\n name: \"tokens-per-edit\",\n value: Math.round(ratio),\n status: ratio <= 5000 ? \"healthy\" : ratio <= 15000 ? \"warning\" : \"critical\",\n label: Math.round(ratio).toLocaleString(\"en-US\"),\n };\n}\n","/**\n * Composite grading from all 7 quality metrics.\n *\n * Each metric is scored 0-100 based on its thresholds, then weighted\n * into a composite score mapped to a letter grade A+ through F.\n */\n\nimport type { GradeResult, MetricResult, Session } from \"../parser/types.js\";\nimport { computeReadsPerEdit } from \"./reads-per-edit.js\";\nimport { computeRewriteRatio } from \"./rewrite-ratio.js\";\nimport { computeCacheHitRate } from \"./cache-hit-rate.js\";\nimport { computeTaskCompletion } from \"./task-completion.js\";\nimport { computeRetryDensity } from \"./retry-density.js\";\nimport { computeToolDiversity } from \"./tool-diversity.js\";\nimport { computeTokensPerEdit } from \"./tokens-per-edit.js\";\n\ninterface MetricWeight {\n compute: (session: Session) => MetricResult;\n weight: number;\n /** Convert metric value to 0-100 score. Higher is better. */\n score: (value: number) => number;\n}\n\nconst METRIC_WEIGHTS: MetricWeight[] = [\n {\n compute: computeReadsPerEdit,\n weight: 0.2,\n // 0 reads → 0, 2 reads → 50, 4+ reads → 100\n score: (v) => clamp(v / 4 * 100, 0, 100),\n },\n {\n compute: computeRewriteRatio,\n weight: 0.15,\n // 0 ratio → 100, 0.25 → 50, 0.5+ → 0 (inverted: lower is better)\n score: (v) => clamp((1 - v / 0.5) * 100, 0, 100),\n },\n {\n compute: computeCacheHitRate,\n weight: 0.15,\n // 0% → 0, 50% → 100\n score: (v) => clamp(v / 0.5 * 100, 0, 100),\n },\n {\n compute: computeTaskCompletion,\n weight: 0.15,\n // 0.7 → 0, 0.9 → 50, 1.0 → 100\n score: (v) => clamp((v - 0.7) / 0.3 * 100, 0, 100),\n },\n {\n compute: computeRetryDensity,\n weight: 0.1,\n // 0% → 100, 10% → 60, 25%+ → 0 (inverted)\n score: (v) => clamp((1 - v / 0.25) * 100, 0, 100),\n },\n {\n compute: computeToolDiversity,\n weight: 0.1,\n // 0 → 0, 0.4 → 50, 0.6+ → 100\n score: (v) => clamp(v / 0.6 * 100, 0, 100),\n },\n {\n compute: computeTokensPerEdit,\n weight: 0.15,\n // 5000 → 100, 10000 → 50, 15000+ → 0 (inverted)\n score: (v) => clamp((1 - (v - 5000) / 10000) * 100, 0, 100),\n },\n];\n\nconst GRADE_THRESHOLDS: Array<[number, string]> = [\n [97, \"A+\"],\n [93, \"A\"],\n [90, \"A-\"],\n [87, \"B+\"],\n [83, \"B\"],\n [80, \"B-\"],\n [77, \"C+\"],\n [73, \"C\"],\n [70, \"C-\"],\n [67, \"D+\"],\n [63, \"D\"],\n [60, \"D-\"],\n [0, \"F\"],\n];\n\nexport function gradeSession(session: Session): GradeResult {\n const metrics: MetricResult[] = [];\n let weightedSum = 0;\n let totalWeight = 0;\n\n for (const mw of METRIC_WEIGHTS) {\n const result = mw.compute(session);\n metrics.push(result);\n\n if (result.value !== null) {\n weightedSum += mw.score(result.value) * mw.weight;\n totalWeight += mw.weight;\n }\n }\n\n // Normalize if some metrics returned null (insufficient data)\n const compositeScore = totalWeight > 0 ? weightedSum / totalWeight : 0;\n\n const letter =\n GRADE_THRESHOLDS.find(([threshold]) => compositeScore >= threshold)?.[1] ?? \"F\";\n\n return {\n letter,\n score: Math.round(compositeScore),\n metrics,\n };\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, value));\n}\n","/**\n * Terminal output formatting using chalk and cli-table3.\n */\n\nimport chalk from \"chalk\";\nimport Table from \"cli-table3\";\nimport type { GradeResult, MetricResult, Session } from \"../parser/types.js\";\nimport type { RegressionResult } from \"../anomaly/regression-detector.js\";\nimport type { CacheCheckResult } from \"../anomaly/cache-anomaly.js\";\nimport { formatDuration, shortSessionId, projectNameFromSlug, formatNumber } from \"../utils/format.js\";\nimport { getAllTips } from \"./tips.js\";\n\nconst STATUS_ICONS: Record<string, string> = {\n healthy: chalk.green(\"✓\"),\n warning: chalk.yellow(\"⚠\"),\n critical: chalk.red(\"✗\"),\n};\n\nconst STATUS_LABELS: Record<string, string> = {\n healthy: chalk.green(\"healthy\"),\n warning: chalk.yellow(\"warning\"),\n critical: chalk.red(\"critical\"),\n};\n\nconst METRIC_DISPLAY_NAMES: Record<string, string> = {\n \"reads-per-edit\": \"Reads/edit\",\n \"rewrite-ratio\": \"Rewrite ratio\",\n \"cache-hit-rate\": \"Cache hit rate\",\n \"task-completion\": \"Task completion\",\n \"retry-density\": \"Retry density\",\n \"tool-diversity\": \"Tool diversity\",\n \"tokens-per-edit\": \"Tokens/useful-edit\",\n};\n\nexport function renderAuditReport(session: Session, grade: GradeResult): string {\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(chalk.bold(\" inspecto v1.0.0\") + chalk.dim(\" — Claude Code Session Quality Analyzer\"));\n lines.push(\"\");\n\n const sessionInfo = [\n `Session: ${chalk.cyan(shortSessionId(session.id))}`,\n projectNameFromSlug(session.projectSlug),\n formatDuration(session.durationMs),\n session.model,\n ].join(chalk.dim(\" | \"));\n lines.push(` ${sessionInfo}`);\n lines.push(\"\");\n\n const gradeColor = getGradeColor(grade.letter);\n lines.push(` Overall grade: ${gradeColor(chalk.bold(grade.letter))}`);\n lines.push(\"\");\n\n const table = new Table({\n head: [\"Metric\", \"Value\", \"Status\"].map((h) => chalk.dim(h)),\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const metric of grade.metrics) {\n const displayName = METRIC_DISPLAY_NAMES[metric.name] ?? metric.name;\n const icon = STATUS_ICONS[metric.status] ?? \"\";\n table.push([displayName, metric.label, `${icon} ${STATUS_LABELS[metric.status] ?? metric.status}`]);\n }\n\n lines.push(table.toString());\n\n const tips = getAllTips(grade.metrics);\n if (tips.length > 0) {\n lines.push(\"\");\n lines.push(chalk.yellow(\" Tips:\"));\n for (const tip of tips) {\n lines.push(` ${chalk.dim(\"→\")} ${tip}`);\n }\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nexport function renderTrendReport(\n results: RegressionResult[],\n sessionCount: number,\n period: string,\n): string {\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(chalk.bold(` Trend report: last ${period}`) + chalk.dim(` (${sessionCount} sessions)`));\n lines.push(\"\");\n\n const table = new Table({\n head: [\"Metric\", \"Recent avg\", \"Full avg\", \"Change\", \"Status\"].map((h) => chalk.dim(h)),\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const result of results) {\n const displayName = METRIC_DISPLAY_NAMES[result.name] ?? result.name;\n const recentStr = result.recentAvg !== null ? result.recentAvg.toFixed(2) : \"N/A\";\n const fullStr = result.fullAvg !== null ? result.fullAvg.toFixed(2) : \"N/A\";\n\n let changeStr = \"N/A\";\n if (result.changePercent !== null) {\n const arrow = result.changePercent > 0 ? \"▲\" : result.changePercent < 0 ? \"▼\" : \"\";\n changeStr = `${arrow} ${Math.abs(Math.round(result.changePercent))}%`;\n }\n\n const statusStr = formatRegressionStatus(result.status);\n table.push([displayName, recentStr, fullStr, changeStr, statusStr]);\n }\n\n lines.push(table.toString());\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nexport function renderCacheCheckReport(results: CacheCheckResult[]): string {\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(chalk.bold(\" Cache health check\"));\n lines.push(\"\");\n\n const table = new Table({\n head: [\"Session\", \"Project\", \"Cache Hit\", \"Status\"].map((h) => chalk.dim(h)),\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const result of results) {\n const hitStr = result.cacheHitRate !== null ? result.cacheHitRate.toFixed(2) : \"N/A\";\n const statusStr = result.isAnomaly\n ? chalk.red(\"✗ ANOMALY\")\n : chalk.green(\"✓ normal\");\n\n table.push([\n shortSessionId(result.sessionId),\n projectNameFromSlug(result.projectSlug),\n hitStr,\n statusStr,\n ]);\n }\n\n lines.push(table.toString());\n\n const anomalies = results.filter((r) => r.isAnomaly);\n if (anomalies.length > 0) {\n lines.push(\"\");\n lines.push(\n chalk.yellow(` ⚠ ${anomalies.length} session(s) with abnormally low cache hit rate.`),\n );\n for (const a of anomalies) {\n if (a.estimatedInflation) {\n lines.push(\n ` ${chalk.dim(\"→\")} Session ${shortSessionId(a.sessionId)} consumed ~${a.estimatedInflation}x more input tokens than expected.`,\n );\n }\n }\n lines.push(\n chalk.dim(\" Try: restart Claude Code or downgrade to a previous version.\"),\n );\n } else {\n lines.push(\"\");\n lines.push(chalk.green(\" ✓ No cache anomalies detected.\"));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nfunction getGradeColor(letter: string): (text: string) => string {\n if (letter.startsWith(\"A\")) return chalk.green;\n if (letter.startsWith(\"B\")) return chalk.cyan;\n if (letter.startsWith(\"C\")) return chalk.yellow;\n return chalk.red;\n}\n\nfunction formatRegressionStatus(status: string): string {\n switch (status) {\n case \"stable\":\n return chalk.green(\"✓ stable\");\n case \"declining\":\n return chalk.yellow(\"⚠ declining\");\n case \"regression\":\n return chalk.red(\"⚠ REGRESSION\");\n default:\n return status;\n }\n}\n","/**\n * Number and string formatting helpers for terminal output.\n */\n\n/** Format a number with comma separators: 3218 → \"3,218\" */\nexport function formatNumber(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\n/** Format a ratio as a fixed-2 decimal: 0.734 → \"0.73\" */\nexport function formatRatio(n: number): string {\n return n.toFixed(2);\n}\n\n/** Format a percentage: 0.734 → \"73%\" */\nexport function formatPercent(n: number): string {\n return `${Math.round(n * 100)}%`;\n}\n\n/** Format milliseconds into a human-readable duration: 2820000 → \"47 min\" */\nexport function formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} min`;\n\n const hours = Math.floor(minutes / 60);\n const remainingMinutes = minutes % 60;\n if (remainingMinutes === 0) return `${hours}h`;\n return `${hours}h ${remainingMinutes}m`;\n}\n\n/** Truncate a session ID for display: \"31f3f224-abcd-...\" → \"31f3f224\" */\nexport function shortSessionId(id: string): string {\n return id.slice(0, 8);\n}\n\n/** Extract a human-readable project name from a slug like \"-Users-foo-my-app\" */\nexport function projectNameFromSlug(slug: string): string {\n const parts = slug.split(\"-\").filter(Boolean);\n return parts[parts.length - 1] || slug;\n}\n","/**\n * Contextual tips based on metric values.\n * Maps poor-scoring metrics to actionable suggestions.\n */\n\nimport type { MetricResult } from \"../parser/types.js\";\n\nconst TIPS: Record<string, Record<string, string>> = {\n \"reads-per-edit\": {\n warning: \"Claude is editing with less context. Add 'Always read files before editing' to your CLAUDE.md.\",\n critical: \"Very low reads before edits. Claude is making blind changes. Consider adding explicit read instructions.\",\n },\n \"rewrite-ratio\": {\n warning: \"High ratio of full-file rewrites. Add 'Prefer Edit over Write for existing files' to CLAUDE.md.\",\n critical: \"Claude is rewriting entire files instead of making surgical edits. This wastes tokens and risks data loss.\",\n },\n \"cache-hit-rate\": {\n warning: \"Cache hit rate is below normal. Sessions may be too short for caching to help.\",\n critical: \"Very low cache hit rate — possible cache bug. Try restarting Claude Code or downgrading to a previous version.\",\n },\n \"task-completion\": {\n warning: \"Claude is occasionally promising actions without following through.\",\n critical: \"Frequent unfulfilled promises. Claude says it will do things but doesn't. Try breaking tasks into smaller steps.\",\n },\n \"retry-density\": {\n warning: \"Some user messages look like retries. Claude may be misunderstanding requests.\",\n critical: \"High retry rate. Users are frequently re-asking. Consider providing more context in prompts.\",\n },\n \"tool-diversity\": {\n warning: \"Low tool diversity. Claude is over-relying on a narrow set of tools.\",\n critical: \"Very narrow tool usage. Claude may be stuck in a pattern. Try prompting for specific tool usage.\",\n },\n \"tokens-per-edit\": {\n warning: \"Tokens per edit is above average. Claude may be verbose without being productive.\",\n critical: \"Very high token cost per edit. Claude is burning tokens without proportional output.\",\n },\n};\n\nexport function getTip(metric: MetricResult): string | null {\n if (metric.status === \"healthy\") return null;\n\n const metricTips = TIPS[metric.name];\n if (!metricTips) return null;\n\n return metricTips[metric.status] ?? null;\n}\n\nexport function getAllTips(metrics: MetricResult[]): string[] {\n return metrics\n .map(getTip)\n .filter((tip): tip is string => tip !== null);\n}\n","/**\n * JSON output mode for scripting and CI.\n */\n\nimport type { GradeResult, Session } from \"../parser/types.js\";\nimport type { RegressionResult } from \"../anomaly/regression-detector.js\";\nimport type { CacheCheckResult } from \"../anomaly/cache-anomaly.js\";\n\nexport interface AuditJsonOutput {\n session: {\n id: string;\n project: string;\n model: string;\n durationMs: number;\n startTime: string;\n };\n grade: string;\n score: number;\n metrics: Array<{\n name: string;\n value: number | null;\n status: string;\n label: string;\n }>;\n}\n\nexport function formatAuditJson(session: Session, grade: GradeResult): string {\n const output: AuditJsonOutput = {\n session: {\n id: session.id,\n project: session.projectSlug,\n model: session.model,\n durationMs: session.durationMs,\n startTime: session.startTime,\n },\n grade: grade.letter,\n score: grade.score,\n metrics: grade.metrics.map((m) => ({\n name: m.name,\n value: m.value,\n status: m.status,\n label: m.label,\n })),\n };\n\n return JSON.stringify(output, null, 2);\n}\n\nexport function formatTrendJson(results: RegressionResult[]): string {\n return JSON.stringify({ trend: results }, null, 2);\n}\n\nexport function formatCacheCheckJson(results: CacheCheckResult[]): string {\n return JSON.stringify({ cacheCheck: results }, null, 2);\n}\n","/**\n * Default command — grade the most recent session.\n */\n\nimport { getMostRecentSession } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { gradeSession } from \"../metrics/grader.js\";\nimport { renderAuditReport } from \"../reporter/terminal.js\";\nimport { formatAuditJson } from \"../reporter/json-reporter.js\";\n\nexport interface AuditOptions {\n json?: boolean;\n verbose?: boolean;\n dataDir?: string;\n project?: string;\n}\n\nexport async function runAudit(options: AuditOptions): Promise<void> {\n const sessionFile = await getMostRecentSession({\n dataDir: options.dataDir,\n project: options.project,\n });\n\n const records = readJsonl(sessionFile.path);\n const session = await buildSession(\n records,\n sessionFile.sessionId,\n sessionFile.projectSlug,\n );\n\n const grade = gradeSession(session);\n\n if (options.json) {\n console.log(formatAuditJson(session, grade));\n } else {\n console.log(renderAuditReport(session, grade));\n }\n}\n","/**\n * Compute rolling averages from multiple sessions for trend analysis.\n */\n\nimport type { GradeResult } from \"../parser/types.js\";\n\nexport interface MetricAverage {\n name: string;\n recentAvg: number | null;\n fullAvg: number | null;\n changePercent: number | null;\n}\n\n/**\n * Compute per-metric averages for a recent window vs. full range.\n * @param grades - All graded sessions, sorted most recent first\n * @param recentCount - Number of sessions in the \"recent\" window\n */\nexport function computeBaselines(\n grades: GradeResult[],\n recentCount: number,\n): MetricAverage[] {\n if (grades.length === 0) return [];\n\n const recent = grades.slice(0, recentCount);\n const full = grades;\n\n const metricNames = grades[0].metrics.map((m) => m.name);\n\n return metricNames.map((name) => {\n const recentValues = extractValues(recent, name);\n const fullValues = extractValues(full, name);\n\n const recentAvg = average(recentValues);\n const fullAvg = average(fullValues);\n\n let changePercent: number | null = null;\n if (recentAvg !== null && fullAvg !== null && fullAvg !== 0) {\n changePercent = ((recentAvg - fullAvg) / Math.abs(fullAvg)) * 100;\n }\n\n return { name, recentAvg, fullAvg, changePercent };\n });\n}\n\nfunction extractValues(grades: GradeResult[], metricName: string): number[] {\n const values: number[] = [];\n for (const grade of grades) {\n const metric = grade.metrics.find((m) => m.name === metricName);\n if (metric?.value !== null && metric?.value !== undefined) {\n values.push(metric.value);\n }\n }\n return values;\n}\n\nfunction average(values: number[]): number | null {\n if (values.length === 0) return null;\n return values.reduce((a, b) => a + b, 0) / values.length;\n}\n","/**\n * Z-score based regression detection.\n *\n * Compares recent metric values against historical baseline to flag\n * statistically significant regressions.\n */\n\nimport type { MetricAverage } from \"./baseline.js\";\n\nexport type RegressionStatus = \"stable\" | \"declining\" | \"regression\";\n\nexport interface RegressionResult {\n name: string;\n recentAvg: number | null;\n fullAvg: number | null;\n changePercent: number | null;\n status: RegressionStatus;\n}\n\n/** Metrics where HIGHER values are WORSE (inverted for regression detection). */\nconst INVERTED_METRICS = new Set([\n \"rewrite-ratio\",\n \"retry-density\",\n \"tokens-per-edit\",\n]);\n\n/**\n * Detect regressions from baseline averages.\n * A change > 30% in the \"bad\" direction is a regression.\n * A change > 10% is \"declining\".\n */\nexport function detectRegressions(\n baselines: MetricAverage[],\n): RegressionResult[] {\n return baselines.map((b) => {\n let status: RegressionStatus = \"stable\";\n\n if (b.changePercent !== null) {\n const isInverted = INVERTED_METRICS.has(b.name);\n // For normal metrics, negative change is bad. For inverted, positive change is bad.\n const badDirection = isInverted ? b.changePercent > 0 : b.changePercent < 0;\n const magnitude = Math.abs(b.changePercent);\n\n if (badDirection && magnitude > 30) {\n status = \"regression\";\n } else if (badDirection && magnitude > 10) {\n status = \"declining\";\n }\n }\n\n return {\n name: b.name,\n recentAvg: b.recentAvg,\n fullAvg: b.fullAvg,\n changePercent: b.changePercent,\n status,\n };\n });\n}\n","/**\n * Parse human-readable duration strings into Date offsets.\n */\n\n/**\n * Parse a duration string like \"7d\", \"14d\", \"30d\" into a Date\n * representing that many days before `now`.\n */\nexport function parseDuration(duration: string, now = new Date()): Date {\n const match = duration.match(/^(\\d+)d$/);\n if (!match) {\n throw new Error(\n `Invalid duration: \"${duration}\". Use format like \"7d\", \"14d\", \"30d\".`,\n );\n }\n\n const days = parseInt(match[1], 10);\n const result = new Date(now);\n result.setDate(result.getDate() - days);\n return result;\n}\n","/**\n * Trend analysis command — detect regressions over time.\n */\n\nimport { scanSessions } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { gradeSession } from \"../metrics/grader.js\";\nimport { computeBaselines } from \"../anomaly/baseline.js\";\nimport { detectRegressions } from \"../anomaly/regression-detector.js\";\nimport { renderTrendReport } from \"../reporter/terminal.js\";\nimport { formatTrendJson } from \"../reporter/json-reporter.js\";\nimport { parseDuration } from \"../utils/duration.js\";\nimport type { GradeResult } from \"../parser/types.js\";\n\nexport interface TrendOptions {\n since?: string;\n json?: boolean;\n dataDir?: string;\n project?: string;\n}\n\nexport async function runTrend(options: TrendOptions): Promise<void> {\n const duration = options.since ?? \"7d\";\n const sinceDate = parseDuration(duration);\n\n const sessionFiles = await scanSessions({\n dataDir: options.dataDir,\n project: options.project,\n since: sinceDate,\n });\n\n if (sessionFiles.length === 0) {\n console.log(`No sessions found in the last ${duration}.`);\n return;\n }\n\n const grades: GradeResult[] = [];\n for (const sf of sessionFiles) {\n try {\n const records = readJsonl(sf.path);\n const session = await buildSession(records, sf.sessionId, sf.projectSlug);\n grades.push(gradeSession(session));\n } catch {\n // Skip sessions that fail to parse\n }\n }\n\n if (grades.length === 0) {\n console.log(\"No valid sessions found to analyze.\");\n return;\n }\n\n // Use half the sessions as the \"recent\" window, minimum 1\n const recentCount = Math.max(1, Math.floor(grades.length / 2));\n const baselines = computeBaselines(grades, recentCount);\n const regressions = detectRegressions(baselines);\n\n if (options.json) {\n console.log(formatTrendJson(regressions));\n } else {\n console.log(renderTrendReport(regressions, grades.length, duration));\n }\n}\n","/**\n * Cache hit rate anomaly detection.\n *\n * Specifically checks for the prompt cache bug that caused 10-20x\n * token cost inflation by detecting sessions with near-zero cache hit rates.\n */\n\nimport type { Session } from \"../parser/types.js\";\nimport { computeCacheHitRate } from \"../metrics/cache-hit-rate.js\";\n\nexport interface CacheCheckResult {\n sessionId: string;\n projectSlug: string;\n timestamp: string;\n cacheHitRate: number | null;\n isAnomaly: boolean;\n estimatedInflation: number | null;\n}\n\nconst ANOMALY_THRESHOLD = 0.05;\nconst NORMAL_CACHE_RATE = 0.65;\n\n/**\n * Check a single session for cache hit rate anomalies.\n */\nexport function checkCacheAnomaly(session: Session): CacheCheckResult {\n const metric = computeCacheHitRate(session);\n\n const isAnomaly = metric.value !== null && metric.value < ANOMALY_THRESHOLD;\n\n let estimatedInflation: number | null = null;\n if (isAnomaly && metric.value !== null) {\n // If normal rate is 65% cache reads, the effective input cost multiplier\n // when cache is broken is roughly 1 / (1 - normalRate)\n // Normal: 35% full-price tokens. Broken: 100% full-price tokens.\n estimatedInflation = Math.round(1 / (1 - NORMAL_CACHE_RATE));\n }\n\n return {\n sessionId: session.id,\n projectSlug: session.projectSlug,\n timestamp: session.startTime,\n cacheHitRate: metric.value,\n isAnomaly,\n estimatedInflation,\n };\n}\n","/**\n * Cache bug detection command.\n * Scans recent sessions for abnormally low cache hit rates.\n */\n\nimport { scanSessions } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { checkCacheAnomaly } from \"../anomaly/cache-anomaly.js\";\nimport { renderCacheCheckReport } from \"../reporter/terminal.js\";\nimport { formatCacheCheckJson } from \"../reporter/json-reporter.js\";\nimport { parseDuration } from \"../utils/duration.js\";\nimport type { CacheCheckResult } from \"../anomaly/cache-anomaly.js\";\n\nexport interface CacheCheckOptions {\n since?: string;\n json?: boolean;\n dataDir?: string;\n}\n\nexport async function runCacheCheck(options: CacheCheckOptions): Promise<void> {\n const duration = options.since ?? \"7d\";\n const sinceDate = parseDuration(duration);\n\n const sessionFiles = await scanSessions({\n dataDir: options.dataDir,\n since: sinceDate,\n });\n\n if (sessionFiles.length === 0) {\n console.log(`No sessions found in the last ${duration}.`);\n return;\n }\n\n const results: CacheCheckResult[] = [];\n for (const sf of sessionFiles) {\n try {\n const records = readJsonl(sf.path);\n const session = await buildSession(records, sf.sessionId, sf.projectSlug);\n results.push(checkCacheAnomaly(session));\n } catch {\n // Skip sessions that fail to parse\n }\n }\n\n if (results.length === 0) {\n console.log(\"No valid sessions found to analyze.\");\n return;\n }\n\n if (options.json) {\n console.log(formatCacheCheckJson(results));\n } else {\n console.log(renderCacheCheckReport(results));\n }\n}\n","/**\n * Cross-project comparison command.\n * Compares average quality metrics across multiple projects.\n */\n\nimport { scanSessions } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { gradeSession } from \"../metrics/grader.js\";\nimport chalk from \"chalk\";\nimport Table from \"cli-table3\";\nimport type { GradeResult } from \"../parser/types.js\";\nimport { projectNameFromSlug } from \"../utils/format.js\";\n\nexport interface CompareOptions {\n projects: string;\n json?: boolean;\n dataDir?: string;\n since?: string;\n}\n\ninterface ProjectSummary {\n name: string;\n sessionCount: number;\n avgGrade: number;\n avgLetter: string;\n metrics: Map<string, number>;\n}\n\nexport async function runCompare(options: CompareOptions): Promise<void> {\n const projectNames = options.projects.split(\",\").map((p) => p.trim());\n const summaries: ProjectSummary[] = [];\n\n for (const projectFilter of projectNames) {\n const sessionFiles = await scanSessions({\n dataDir: options.dataDir,\n project: projectFilter,\n });\n\n if (sessionFiles.length === 0) continue;\n\n const grades: GradeResult[] = [];\n for (const sf of sessionFiles) {\n try {\n const records = readJsonl(sf.path);\n const session = await buildSession(records, sf.sessionId, sf.projectSlug);\n grades.push(gradeSession(session));\n } catch {\n continue;\n }\n }\n\n if (grades.length === 0) continue;\n\n const avgScore = grades.reduce((s, g) => s + g.score, 0) / grades.length;\n const metricAvgs = new Map<string, number>();\n for (const metric of grades[0].metrics) {\n const values = grades\n .map((g) => g.metrics.find((m) => m.name === metric.name)?.value)\n .filter((v): v is number => v !== null);\n if (values.length > 0) {\n metricAvgs.set(metric.name, values.reduce((a, b) => a + b, 0) / values.length);\n }\n }\n\n summaries.push({\n name: projectFilter,\n sessionCount: grades.length,\n avgGrade: Math.round(avgScore),\n avgLetter: getLetterGrade(avgScore),\n metrics: metricAvgs,\n });\n }\n\n if (summaries.length === 0) {\n console.log(\"No matching projects found.\");\n return;\n }\n\n if (options.json) {\n const jsonOutput = summaries.map((s) => ({\n project: s.name,\n sessions: s.sessionCount,\n grade: s.avgLetter,\n score: s.avgGrade,\n metrics: Object.fromEntries(s.metrics),\n }));\n console.log(JSON.stringify({ compare: jsonOutput }, null, 2));\n return;\n }\n\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(\" Project comparison\"));\n lines.push(\"\");\n\n const head = [\"Project\", \"Sessions\", \"Grade\", ...summaries[0]?.metrics.keys() ?? []].map(\n (h) => chalk.dim(h),\n );\n\n const table = new Table({\n head,\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const summary of summaries) {\n const row: string[] = [\n summary.name,\n summary.sessionCount.toString(),\n summary.avgLetter,\n ];\n for (const [, value] of summary.metrics) {\n row.push(value.toFixed(2));\n }\n table.push(row);\n }\n\n lines.push(table.toString());\n lines.push(\"\");\n console.log(lines.join(\"\\n\"));\n}\n\nfunction getLetterGrade(score: number): string {\n if (score >= 97) return \"A+\";\n if (score >= 93) return \"A\";\n if (score >= 90) return \"A-\";\n if (score >= 87) return \"B+\";\n if (score >= 83) return \"B\";\n if (score >= 80) return \"B-\";\n if (score >= 77) return \"C+\";\n if (score >= 73) return \"C\";\n if (score >= 70) return \"C-\";\n if (score >= 67) return \"D+\";\n if (score >= 63) return \"D\";\n if (score >= 60) return \"D-\";\n return \"F\";\n}\n"],"mappings":"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","names":["join","join","turns","average","round","round","round","round","round","EDIT_TOOLS","chalk","Table"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/parser/project-scanner.ts","../src/utils/paths.ts","../src/parser/jsonl-reader.ts","../src/parser/session-builder.ts","../src/metrics/reads-per-edit.ts","../src/metrics/rewrite-ratio.ts","../src/metrics/cache-hit-rate.ts","../src/metrics/task-completion.ts","../src/utils/levenshtein.ts","../src/metrics/retry-density.ts","../src/metrics/tool-diversity.ts","../src/metrics/tokens-per-edit.ts","../src/metrics/subagent-overhead.ts","../src/metrics/grader.ts","../src/reporter/terminal.ts","../src/utils/format.ts","../src/reporter/tips.ts","../src/reporter/json-reporter.ts","../src/commands/audit.ts","../src/anomaly/baseline.ts","../src/anomaly/regression-detector.ts","../src/utils/duration.ts","../src/commands/trend.ts","../src/anomaly/cache-anomaly.ts","../src/commands/cache-check.ts","../src/commands/compare.ts"],"sourcesContent":["/**\n * inspecto — Claude Code Session Quality Analyzer\n *\n * Grade sessions, detect regressions, catch cache bugs.\n * All from the JSONL logs Claude Code already writes.\n */\n\nimport { Command } from \"commander\";\nimport { runAudit } from \"./commands/audit.js\";\nimport { runTrend } from \"./commands/trend.js\";\nimport { runCacheCheck } from \"./commands/cache-check.js\";\nimport { runCompare } from \"./commands/compare.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"inspecto\")\n .description(\"Claude Code session quality analyzer — grade sessions, detect regressions, catch cache bugs\")\n .version(\"1.0.0\");\n\nprogram\n .command(\"audit\", { isDefault: true })\n .description(\"Grade the most recent Claude Code session\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--verbose\", \"Show per-message breakdown\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .option(\"--project <name>\", \"Filter to a specific project\")\n .action(async (options) => {\n try {\n await runAudit(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"trend\")\n .description(\"Analyze quality trends and detect regressions over time\")\n .option(\"--since <duration>\", \"Time range: 7d, 14d, 30d\", \"7d\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .option(\"--project <name>\", \"Filter to a specific project\")\n .action(async (options) => {\n try {\n await runTrend(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"cache-check\")\n .description(\"Detect prompt cache bugs that inflate token costs\")\n .option(\"--since <duration>\", \"Time range: 7d, 14d, 30d\", \"7d\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .action(async (options) => {\n try {\n await runCacheCheck(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nprogram\n .command(\"compare\")\n .description(\"Compare quality metrics across projects\")\n .requiredOption(\"--projects <names>\", \"Comma-separated project names\")\n .option(\"--json\", \"Output as JSON\")\n .option(\"--data-dir <path>\", \"Custom Claude data directory\")\n .option(\"--since <duration>\", \"Time range: 7d, 14d, 30d\")\n .action(async (options) => {\n try {\n await runCompare(options);\n } catch (error) {\n handleError(error);\n }\n });\n\nfunction handleError(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`\\nError: ${message}\\n`);\n process.exit(1);\n}\n\nprogram.parse();\n","/**\n * Discovers Claude Code session files under ~/.claude/projects/.\n *\n * Session files are at: ~/.claude/projects/{project-slug}/{sessionId}.jsonl\n * Subagent files are at: ~/.claude/projects/{project-slug}/{sessionId}/subagents/agent-*.jsonl\n */\n\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join, basename, extname } from \"node:path\";\nimport { getClaudeDir } from \"../utils/paths.js\";\nimport type { SessionFile } from \"./types.js\";\n\n/**\n * Scan ~/.claude/projects/ for all main session JSONL files.\n * Returns files sorted by modification time (most recent first).\n */\nexport async function scanSessions(options?: {\n dataDir?: string;\n project?: string;\n since?: Date;\n}): Promise<SessionFile[]> {\n const claudeDir = options?.dataDir ?? getClaudeDir();\n const projectsDir = join(claudeDir, \"projects\");\n\n let projectDirs: string[];\n try {\n projectDirs = await readdir(projectsDir);\n } catch {\n throw new Error(\n \"Claude Code data directory not found. \" +\n \"Make sure Claude Code is installed and has been used at least once.\\n\" +\n `Expected: ${projectsDir}`,\n );\n }\n\n // Filter to specific project if requested\n if (options?.project) {\n projectDirs = projectDirs.filter((dir) =>\n dir.toLowerCase().includes(options.project!.toLowerCase()),\n );\n }\n\n const projectResults = await Promise.all(\n projectDirs\n .filter((dir) => !dir.startsWith(\".\"))\n .map(async (projectDir) => {\n const fullProjectDir = join(projectsDir, projectDir);\n let entries: string[];\n try {\n entries = await readdir(fullProjectDir);\n } catch {\n return [] as SessionFile[];\n }\n\n const fileResults = await Promise.all(\n entries\n .filter((entry) => extname(entry) === \".jsonl\")\n .map(async (entry) => {\n const filePath = join(fullProjectDir, entry);\n const sessionId = basename(entry, \".jsonl\");\n try {\n const fileStat = await stat(filePath);\n if (options?.since && fileStat.mtime < options.since) return null;\n\n let subagentPaths: string[] | undefined;\n try {\n const subagentDir = join(fullProjectDir, sessionId, \"subagents\");\n const agentFiles = await readdir(subagentDir);\n const paths = agentFiles\n .filter((f) => f.startsWith(\"agent-\") && f.endsWith(\".jsonl\"))\n .map((f) => join(subagentDir, f));\n if (paths.length > 0) subagentPaths = paths;\n } catch {\n // No subagents directory — normal for older sessions\n }\n\n return {\n path: filePath,\n sessionId,\n projectSlug: projectDir,\n mtime: fileStat.mtime,\n subagentPaths,\n } as SessionFile;\n } catch {\n return null;\n }\n }),\n );\n return fileResults.filter((f): f is SessionFile => f !== null);\n }),\n );\n\n const sessions: SessionFile[] = projectResults.flat();\n\n // Sort most recent first\n sessions.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n return sessions;\n}\n\n/**\n * Get the most recent session file, optionally filtered by project.\n */\nexport async function getMostRecentSession(options?: {\n dataDir?: string;\n project?: string;\n}): Promise<SessionFile> {\n const sessions = await scanSessions(options);\n if (sessions.length === 0) {\n throw new Error(\n \"No Claude Code sessions found. \" +\n \"Use Claude Code in a project first to generate session data.\",\n );\n }\n return sessions[0];\n}\n","/**\n * Cross-platform path resolution for Claude Code data directories.\n */\n\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/**\n * Returns the Claude Code data directory.\n * macOS/Linux: ~/.claude\n * Windows: %USERPROFILE%\\.claude\n */\nexport function getClaudeDir(): string {\n return join(homedir(), \".claude\");\n}\n","/**\n * Streaming JSONL reader using Node's readline + createReadStream.\n * Processes files line-by-line to handle 100MB+ session files without\n * loading them into memory.\n */\n\nimport { createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport type { RawRecord } from \"./types.js\";\n\n/**\n * Stream-reads a JSONL file, yielding one parsed record per line.\n * Malformed lines are silently skipped (common near session end during crashes).\n */\nexport async function* readJsonl(filePath: string): AsyncGenerator<RawRecord> {\n const stream = createReadStream(filePath, { encoding: \"utf-8\" });\n const rl = createInterface({ input: stream, crlfDelay: Infinity });\n\n for await (const line of rl) {\n const trimmed = line.trim();\n if (trimmed.length === 0) continue;\n\n try {\n const record = JSON.parse(trimmed) as RawRecord;\n if (record && typeof record === \"object\" && \"type\" in record) {\n yield record;\n }\n } catch {\n // Skip malformed lines — common at session boundaries\n }\n }\n}\n","/**\n * Builds a Session from raw JSONL records.\n *\n * Handles the core complexity of Claude Code's streaming format:\n * - Assistant turns are split across multiple JSONL records sharing the same\n * `message.id`. Content blocks from each chunk are merged into one turn.\n * - Only the final chunk (stop_reason != null) has real output_tokens.\n * - Synthetic records (model: \"<synthetic>\") and errored turns are excluded.\n */\n\nimport { basename } from \"node:path\";\nimport { readJsonl } from \"./jsonl-reader.js\";\nimport type {\n AssistantRecord,\n ContentBlock,\n MergedTurn,\n RawRecord,\n Session,\n UsageData,\n UserRecord,\n} from \"./types.js\";\n\ninterface AssistantAccumulator {\n content: ContentBlock[];\n usage: UsageData | null;\n complete: boolean;\n timestamp: string;\n model: string;\n}\n\n/**\n * Build a processed Session from an async stream of raw records.\n * @param records - AsyncIterable of raw JSONL records (from readJsonl)\n * @param sessionId - The session ID (from filename)\n * @param projectSlug - The project slug (from parent directory name)\n * @param subagentPaths - Optional paths to subagent JSONL files to merge in\n */\nexport async function buildSession(\n records: AsyncIterable<RawRecord>,\n sessionId: string,\n projectSlug: string,\n subagentPaths?: string[],\n): Promise<Session> {\n const turns: MergedTurn[] = [];\n\n let cwd = \"\";\n let gitBranch: string | null = null;\n let model = \"\";\n let firstTimestamp = \"\";\n let lastTimestamp = \"\";\n\n async function processRecords(\n stream: AsyncIterable<RawRecord>,\n agentId: string | undefined,\n ) {\n const assistantChunks = new Map<string, AssistantAccumulator>();\n\n for await (const record of stream) {\n if (isSkippable(record.type)) continue;\n\n if (record.type === \"user\") {\n const userRecord = record as UserRecord;\n handleUserRecord(userRecord, agentId);\n captureMetadata(userRecord);\n } else if (record.type === \"assistant\") {\n const assistantRecord = record as AssistantRecord;\n if (assistantRecord.message.model === \"<synthetic>\") continue;\n if (assistantRecord.error) continue;\n handleAssistantChunk(assistantRecord, assistantChunks);\n captureMetadata(assistantRecord);\n }\n }\n\n // Flush all accumulated assistant chunks into turns\n for (const [, acc] of assistantChunks) {\n turns.push({\n role: \"assistant\",\n content: acc.content,\n usage: acc.usage,\n complete: acc.complete,\n timestamp: acc.timestamp,\n isHumanTurn: false,\n model: acc.model,\n agentId,\n });\n }\n }\n\n await processRecords(records, undefined);\n\n for (const agentPath of subagentPaths ?? []) {\n const agentId = basename(agentPath, \".jsonl\");\n await processRecords(readJsonl(agentPath), agentId);\n }\n\n // Sort all turns (main + subagents) by timestamp\n turns.sort((a, b) => a.timestamp.localeCompare(b.timestamp));\n\n const subagentIds = new Set(\n turns.filter((t) => t.agentId !== undefined).map((t) => t.agentId!),\n );\n const subagentTurnCount = turns.filter((t) => t.agentId !== undefined).length;\n\n return {\n id: sessionId,\n projectSlug,\n model,\n turns,\n startTime: firstTimestamp,\n endTime: lastTimestamp,\n cwd,\n gitBranch,\n durationMs:\n firstTimestamp && lastTimestamp\n ? new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime()\n : 0,\n subagentCount: subagentIds.size,\n subagentTurnCount,\n };\n\n // -- Inner helpers --------------------------------------------------------\n\n function captureMetadata(record: UserRecord | AssistantRecord) {\n if (!firstTimestamp && record.timestamp) {\n firstTimestamp = record.timestamp;\n }\n if (record.timestamp) {\n lastTimestamp = record.timestamp;\n }\n if (!cwd && record.cwd) {\n cwd = record.cwd;\n }\n if (gitBranch === null && record.gitBranch) {\n gitBranch = record.gitBranch;\n }\n if (!model && record.type === \"assistant\") {\n const ar = record as AssistantRecord;\n if (ar.message.model && ar.message.model !== \"<synthetic>\") {\n model = ar.message.model;\n }\n }\n }\n\n function handleUserRecord(record: UserRecord, agentId: string | undefined) {\n const content = record.message.content;\n const isHumanTurn = typeof content === \"string\" && !record.isMeta;\n turns.push({\n role: \"user\",\n content: normalizeContent(content),\n usage: null,\n complete: true,\n timestamp: record.timestamp,\n isHumanTurn,\n agentId,\n });\n }\n\n function handleAssistantChunk(\n record: AssistantRecord,\n chunks: Map<string, AssistantAccumulator>,\n ) {\n const messageId = record.message.id;\n let acc = chunks.get(messageId);\n\n if (!acc) {\n acc = {\n content: [],\n usage: null,\n complete: false,\n timestamp: record.timestamp,\n model: record.message.model,\n };\n chunks.set(messageId, acc);\n }\n\n // Append content blocks from this streaming chunk\n for (const block of record.message.content) {\n acc.content.push(block);\n }\n\n // Final chunk has the real usage data\n if (record.message.stop_reason !== null) {\n acc.complete = true;\n acc.usage = record.message.usage;\n }\n }\n}\n\nfunction normalizeContent(content: string | ContentBlock[]): ContentBlock[] {\n if (typeof content === \"string\") {\n return [{ type: \"text\", text: content }];\n }\n return content;\n}\n\nconst SKIPPABLE = new Set([\n \"queue-operation\",\n \"attachment\",\n \"system\",\n \"last-prompt\",\n]);\n\nfunction isSkippable(type: string): boolean {\n return SKIPPABLE.has(type);\n}\n","/**\n * M1: Reads-before-edit ratio.\n *\n * Counts how many Read tool_use events occur before each Write or Edit event.\n * High values mean Claude is reading context before modifying files.\n * The AMD data showed this dropped from 6.6 to 2.0 after March 8.\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nconst EDIT_TOOLS = new Set([\"Write\", \"Edit\", \"NotebookEdit\"]);\nconst READ_TOOL = \"Read\";\n\nexport function computeReadsPerEdit(session: Session): MetricResult {\n let readsSinceLastEdit = 0;\n const ratios: number[] = [];\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n\n if (toolBlock.name === READ_TOOL) {\n readsSinceLastEdit++;\n } else if (EDIT_TOOLS.has(toolBlock.name)) {\n ratios.push(readsSinceLastEdit);\n readsSinceLastEdit = 0;\n }\n }\n }\n\n if (ratios.length === 0) {\n return {\n name: \"reads-per-edit\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No file modifications in this session\",\n };\n }\n\n const average = ratios.reduce((a, b) => a + b, 0) / ratios.length;\n\n return {\n name: \"reads-per-edit\",\n value: round(average),\n status: average >= 4.0 ? \"healthy\" : average >= 2.0 ? \"warning\" : \"critical\",\n label: round(average).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M2: Full-file rewrite ratio.\n *\n * Ratio of Write calls (full file replacement) to total file modifications\n * (Write + Edit). Rising ratio means Claude is rewriting instead of\n * making surgical edits.\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nexport function computeRewriteRatio(session: Session): MetricResult {\n let writes = 0;\n let edits = 0;\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n\n if (toolBlock.name === \"Write\") writes++;\n else if (toolBlock.name === \"Edit\" || toolBlock.name === \"NotebookEdit\") edits++;\n }\n }\n\n const total = writes + edits;\n if (total === 0) {\n return {\n name: \"rewrite-ratio\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No file modifications in this session\",\n };\n }\n\n const ratio = writes / total;\n\n return {\n name: \"rewrite-ratio\",\n value: round(ratio),\n status: ratio <= 0.25 ? \"healthy\" : ratio <= 0.5 ? \"warning\" : \"critical\",\n label: round(ratio).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M3: Cache hit rate.\n *\n * Ratio of cache_read_input_tokens to total input tokens\n * (cache_read + cache_creation). Detects the prompt cache bug\n * that caused 10-20x cost inflation.\n *\n * Note: raw `input_tokens` is always a streaming placeholder (1 or 3).\n * Real input cost = cache_read + cache_creation.\n */\n\nimport type { MetricResult, Session } from \"../parser/types.js\";\n\nexport function computeCacheHitRate(session: Session): MetricResult {\n let totalCacheRead = 0;\n let totalCacheCreation = 0;\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\" || !turn.usage || !turn.complete) continue;\n\n totalCacheRead += turn.usage.cache_read_input_tokens;\n totalCacheCreation += turn.usage.cache_creation_input_tokens;\n }\n\n const totalInput = totalCacheRead + totalCacheCreation;\n if (totalInput === 0) {\n return {\n name: \"cache-hit-rate\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No token usage data available\",\n };\n }\n\n const rate = totalCacheRead / totalInput;\n\n return {\n name: \"cache-hit-rate\",\n value: round(rate),\n status: rate >= 0.5 ? \"healthy\" : rate >= 0.2 ? \"warning\" : \"critical\",\n label: round(rate).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M4: Task completion rate.\n *\n * Detects sessions where Claude says it will do something but doesn't\n * follow through. Looks for intent phrases in assistant text that\n * aren't followed by a tool_use in the next assistant turn.\n */\n\nimport type { MetricResult, Session, MergedTurn, TextBlock, ToolUseBlock } from \"../parser/types.js\";\n\nconst INTENT_PATTERNS = [\n /\\bI'll now\\b/i,\n /\\bLet me\\b/i,\n /\\bI'll update\\b/i,\n /\\bNext,? I'll\\b/i,\n /\\bI'll (?:also |then )?(?:fix|add|create|implement|refactor|modify|change|write|edit|update)\\b/i,\n /\\bI'm going to\\b/i,\n];\n\nexport function computeTaskCompletion(session: Session): MetricResult {\n const assistantTurns = session.turns.filter(\n (t) => t.role === \"assistant\" && t.complete,\n );\n\n let totalIntents = 0;\n let unfulfilledIntents = 0;\n\n for (const turn of assistantTurns) {\n const hasIntent = hasIntentPhrase(turn);\n if (!hasIntent) continue;\n\n totalIntents++;\n\n // An intent is fulfilled if the same merged turn also contains a tool_use.\n // Since streaming chunks are merged, a real action within this turn means\n // Claude followed through. An intent without a tool_use in the same turn\n // is a dangling promise.\n const hasToolUse = turn.content.some((b) => b.type === \"tool_use\");\n if (!hasToolUse) {\n unfulfilledIntents++;\n }\n }\n\n if (totalIntents === 0) {\n return {\n name: \"task-completion\",\n value: 1,\n status: \"healthy\",\n label: \"1.00\",\n detail: \"No intent phrases detected\",\n };\n }\n\n const rate = 1 - unfulfilledIntents / totalIntents;\n\n return {\n name: \"task-completion\",\n value: round(rate),\n status: rate >= 0.9 ? \"healthy\" : rate >= 0.7 ? \"warning\" : \"critical\",\n label: round(rate).toString(),\n };\n}\n\nfunction hasIntentPhrase(turn: MergedTurn): boolean {\n for (const block of turn.content) {\n if (block.type === \"text\") {\n const textBlock = block as TextBlock;\n if (INTENT_PATTERNS.some((p) => p.test(textBlock.text))) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * Levenshtein distance and normalized similarity.\n * Pure implementation — no external dependencies.\n */\n\n/**\n * Compute the Levenshtein edit distance between two strings.\n * Uses a single-row DP approach for O(min(m,n)) space.\n */\nexport function levenshteinDistance(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n // Ensure a is the shorter string for space optimization\n if (a.length > b.length) [a, b] = [b, a];\n\n const aLen = a.length;\n const bLen = b.length;\n const row = new Array<number>(aLen + 1);\n\n for (let i = 0; i <= aLen; i++) row[i] = i;\n\n for (let j = 1; j <= bLen; j++) {\n let prev = row[0];\n row[0] = j;\n\n for (let i = 1; i <= aLen; i++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n const temp = row[i];\n row[i] = Math.min(\n row[i] + 1, // deletion\n row[i - 1] + 1, // insertion\n prev + cost, // substitution\n );\n prev = temp;\n }\n }\n\n return row[aLen];\n}\n\n/**\n * Compute normalized similarity between two strings (0 = different, 1 = identical).\n * Only compares the first `maxLen` characters for performance.\n */\nexport function normalizedSimilarity(\n a: string,\n b: string,\n maxLen = 200,\n): number {\n const aTrunc = a.slice(0, maxLen);\n const bTrunc = b.slice(0, maxLen);\n const maxLength = Math.max(aTrunc.length, bTrunc.length);\n\n if (maxLength === 0) return 1;\n\n const distance = levenshteinDistance(aTrunc, bTrunc);\n return 1 - distance / maxLength;\n}\n","/**\n * M5: Retry density.\n *\n * Measures how often the user sends messages very similar to their\n * previous message — a proxy for \"Claude got it wrong and I'm asking again.\"\n */\n\nimport type { MetricResult, Session, TextBlock } from \"../parser/types.js\";\nimport { normalizedSimilarity } from \"../utils/levenshtein.js\";\n\nexport function computeRetryDensity(session: Session): MetricResult {\n // Extract text from human-authored user turns only\n const humanTexts: string[] = [];\n for (const turn of session.turns) {\n if (!turn.isHumanTurn) continue;\n const text = turn.content\n .filter((b): b is TextBlock => b.type === \"text\")\n .map((b) => b.text)\n .join(\" \");\n if (text.length > 0) humanTexts.push(text);\n }\n\n if (humanTexts.length < 2) {\n return {\n name: \"retry-density\",\n value: 0,\n status: \"healthy\",\n label: \"0.00\",\n detail: \"Not enough user messages to detect retries\",\n };\n }\n\n let retries = 0;\n const pairs = humanTexts.length - 1;\n\n for (let i = 0; i < pairs; i++) {\n const similarity = normalizedSimilarity(humanTexts[i], humanTexts[i + 1]);\n if (similarity > 0.6) {\n retries++;\n }\n }\n\n const density = retries / pairs;\n\n return {\n name: \"retry-density\",\n value: round(density),\n status: density <= 0.1 ? \"healthy\" : density <= 0.25 ? \"warning\" : \"critical\",\n label: round(density).toString(),\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M6: Tool diversity score.\n *\n * Shannon entropy over the distribution of tool_use events by tool name.\n * Normalized to 0-1. Low diversity means Claude is over-relying on\n * one tool (often Write).\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nexport function computeToolDiversity(session: Session): MetricResult {\n const toolCounts = new Map<string, number>();\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n toolCounts.set(toolBlock.name, (toolCounts.get(toolBlock.name) ?? 0) + 1);\n }\n }\n\n const uniqueTools = toolCounts.size;\n if (uniqueTools <= 1) {\n return {\n name: \"tool-diversity\",\n value: uniqueTools === 0 ? null : 0,\n status: uniqueTools === 0 ? \"healthy\" : \"critical\",\n label: uniqueTools === 0 ? \"N/A\" : \"0.00\",\n detail: uniqueTools === 0\n ? \"No tool usage in this session\"\n : `Only one tool used: ${[...toolCounts.keys()][0]}`,\n };\n }\n\n const totalCalls = [...toolCounts.values()].reduce((a, b) => a + b, 0);\n const maxEntropy = Math.log2(uniqueTools);\n\n let entropy = 0;\n for (const count of toolCounts.values()) {\n const p = count / totalCalls;\n entropy -= p * Math.log2(p);\n }\n\n const normalized = entropy / maxEntropy;\n\n // Build detail showing top tools\n const sorted = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);\n const topTool = sorted[0];\n const topPercent = Math.round((topTool[1] / totalCalls) * 100);\n const detail = `Most used: ${topTool[0]} (${topPercent}%)`;\n\n return {\n name: \"tool-diversity\",\n value: round(normalized),\n status: normalized >= 0.6 ? \"healthy\" : normalized >= 0.4 ? \"warning\" : \"critical\",\n label: round(normalized).toString(),\n detail,\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","/**\n * M7: Tokens per useful edit.\n *\n * Total output tokens consumed divided by number of file modification\n * operations (Write + Edit). Rising ratio means Claude is burning more\n * tokens per productive action.\n */\n\nimport type { MetricResult, Session, ToolUseBlock } from \"../parser/types.js\";\n\nconst EDIT_TOOLS = new Set([\"Write\", \"Edit\", \"NotebookEdit\"]);\n\nexport function computeTokensPerEdit(session: Session): MetricResult {\n let totalOutputTokens = 0;\n let editCount = 0;\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\") continue;\n\n // Count tokens from completed turns only\n if (turn.complete && turn.usage) {\n totalOutputTokens += turn.usage.output_tokens;\n }\n\n // Count edit operations\n for (const block of turn.content) {\n if (block.type !== \"tool_use\") continue;\n const toolBlock = block as ToolUseBlock;\n if (EDIT_TOOLS.has(toolBlock.name)) editCount++;\n }\n }\n\n if (editCount === 0) {\n return {\n name: \"tokens-per-edit\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No file modifications in this session\",\n };\n }\n\n const ratio = totalOutputTokens / editCount;\n\n return {\n name: \"tokens-per-edit\",\n value: Math.round(ratio),\n status: ratio <= 5000 ? \"healthy\" : ratio <= 15000 ? \"warning\" : \"critical\",\n label: Math.round(ratio).toLocaleString(\"en-US\"),\n };\n}\n","/**\n * M8: Subagent delegation ratio.\n *\n * Measures what fraction of total output tokens came from the main agent vs\n * subagents. A low main-agent ratio means the orchestrator delegated effectively.\n * Healthy = main agent produced < 60% of total output tokens.\n */\n\nimport type { MetricResult, Session } from \"../parser/types.js\";\n\nexport function computeSubagentOverhead(session: Session): MetricResult {\n if (session.subagentCount === 0) {\n return {\n name: \"subagent-overhead\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No subagents in this session\",\n };\n }\n\n let mainTokens = 0;\n let subagentTokens = 0;\n\n for (const turn of session.turns) {\n if (turn.role !== \"assistant\" || !turn.usage) continue;\n const out = turn.usage.output_tokens ?? 0;\n if (turn.agentId === undefined) {\n mainTokens += out;\n } else {\n subagentTokens += out;\n }\n }\n\n const total = mainTokens + subagentTokens;\n if (total === 0) {\n return {\n name: \"subagent-overhead\",\n value: null,\n status: \"healthy\",\n label: \"N/A\",\n detail: \"No output tokens recorded\",\n };\n }\n\n const ratio = mainTokens / total;\n const status = ratio < 0.6 ? \"healthy\" : ratio < 0.8 ? \"warning\" : \"critical\";\n\n return {\n name: \"subagent-overhead\",\n value: Math.round(ratio * 100) / 100,\n status,\n label: `${Math.round(ratio * 100)}% main`,\n };\n}\n","/**\n * Composite grading from all 8 quality metrics.\n *\n * Each metric is scored 0-100 based on its thresholds, then weighted\n * into a composite score mapped to a letter grade A+ through F.\n */\n\nimport type { GradeResult, MetricResult, Session } from \"../parser/types.js\";\nimport { computeReadsPerEdit } from \"./reads-per-edit.js\";\nimport { computeRewriteRatio } from \"./rewrite-ratio.js\";\nimport { computeCacheHitRate } from \"./cache-hit-rate.js\";\nimport { computeTaskCompletion } from \"./task-completion.js\";\nimport { computeRetryDensity } from \"./retry-density.js\";\nimport { computeToolDiversity } from \"./tool-diversity.js\";\nimport { computeTokensPerEdit } from \"./tokens-per-edit.js\";\nimport { computeSubagentOverhead } from \"./subagent-overhead.js\";\n\ninterface MetricWeight {\n compute: (session: Session) => MetricResult;\n weight: number;\n /** Convert metric value to 0-100 score. Higher is better. */\n score: (value: number) => number;\n}\n\nconst METRIC_WEIGHTS: MetricWeight[] = [\n {\n compute: computeReadsPerEdit,\n weight: 0.2,\n // 0 reads → 0, 2 reads → 50, 4+ reads → 100\n score: (v) => clamp(v / 4 * 100, 0, 100),\n },\n {\n compute: computeRewriteRatio,\n weight: 0.15,\n // 0 ratio → 100, 0.25 → 50, 0.5+ → 0 (inverted: lower is better)\n score: (v) => clamp((1 - v / 0.5) * 100, 0, 100),\n },\n {\n compute: computeCacheHitRate,\n weight: 0.15,\n // 0% → 0, 50% → 100\n score: (v) => clamp(v / 0.5 * 100, 0, 100),\n },\n {\n compute: computeTaskCompletion,\n weight: 0.15,\n // 0.7 → 0, 0.9 → 50, 1.0 → 100\n score: (v) => clamp((v - 0.7) / 0.3 * 100, 0, 100),\n },\n {\n compute: computeRetryDensity,\n weight: 0.1,\n // 0% → 100, 10% → 60, 25%+ → 0 (inverted)\n score: (v) => clamp((1 - v / 0.25) * 100, 0, 100),\n },\n {\n compute: computeToolDiversity,\n weight: 0.05,\n // 0 → 0, 0.4 → 50, 0.6+ → 100\n score: (v) => clamp(v / 0.6 * 100, 0, 100),\n },\n {\n compute: computeTokensPerEdit,\n weight: 0.15,\n // 5000 → 100, 10000 → 50, 15000+ → 0 (inverted)\n score: (v) => clamp((1 - (v - 5000) / 10000) * 100, 0, 100),\n },\n {\n compute: computeSubagentOverhead,\n weight: 0.05,\n // main ratio 0 → 100, 0.6 → 100 (threshold), 0.8 → 50, 1.0 → 0 (inverted)\n score: (v) => clamp((1 - v) / 0.4 * 100, 0, 100),\n },\n];\n\nconst GRADE_THRESHOLDS: Array<[number, string]> = [\n [97, \"A+\"],\n [93, \"A\"],\n [90, \"A-\"],\n [87, \"B+\"],\n [83, \"B\"],\n [80, \"B-\"],\n [77, \"C+\"],\n [73, \"C\"],\n [70, \"C-\"],\n [67, \"D+\"],\n [63, \"D\"],\n [60, \"D-\"],\n [0, \"F\"],\n];\n\nexport function gradeSession(session: Session): GradeResult {\n const metrics: MetricResult[] = [];\n let weightedSum = 0;\n let totalWeight = 0;\n\n for (const mw of METRIC_WEIGHTS) {\n const result = mw.compute(session);\n metrics.push(result);\n\n if (result.value !== null) {\n weightedSum += mw.score(result.value) * mw.weight;\n totalWeight += mw.weight;\n }\n }\n\n // Normalize if some metrics returned null (insufficient data)\n const compositeScore = totalWeight > 0 ? weightedSum / totalWeight : 0;\n\n const letter =\n GRADE_THRESHOLDS.find(([threshold]) => compositeScore >= threshold)?.[1] ?? \"F\";\n\n return {\n letter,\n score: Math.round(compositeScore),\n metrics,\n };\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, value));\n}\n","/**\n * Terminal output formatting using chalk and cli-table3.\n */\n\nimport chalk from \"chalk\";\nimport Table from \"cli-table3\";\nimport type { GradeResult, MetricResult, Session } from \"../parser/types.js\";\nimport type { RegressionResult } from \"../anomaly/regression-detector.js\";\nimport type { CacheCheckResult } from \"../anomaly/cache-anomaly.js\";\nimport { formatDuration, shortSessionId, projectNameFromSlug, formatNumber } from \"../utils/format.js\";\nimport { getAllTips } from \"./tips.js\";\n\nconst STATUS_ICONS: Record<string, string> = {\n healthy: chalk.green(\"✓\"),\n warning: chalk.yellow(\"⚠\"),\n critical: chalk.red(\"✗\"),\n};\n\nconst STATUS_LABELS: Record<string, string> = {\n healthy: chalk.green(\"healthy\"),\n warning: chalk.yellow(\"warning\"),\n critical: chalk.red(\"critical\"),\n};\n\nconst METRIC_DISPLAY_NAMES: Record<string, string> = {\n \"reads-per-edit\": \"Reads/edit\",\n \"rewrite-ratio\": \"Rewrite ratio\",\n \"cache-hit-rate\": \"Cache hit rate\",\n \"task-completion\": \"Task completion\",\n \"retry-density\": \"Retry density\",\n \"tool-diversity\": \"Tool diversity\",\n \"tokens-per-edit\": \"Tokens/useful-edit\",\n \"subagent-overhead\": \"Subagent delegation\",\n};\n\nexport function renderAuditReport(session: Session, grade: GradeResult): string {\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(chalk.bold(\" inspecto v1.0.0\") + chalk.dim(\" — Claude Code Session Quality Analyzer\"));\n lines.push(\"\");\n\n const agentInfo = session.subagentCount > 0\n ? `${session.subagentCount} subagents | ${session.turns.length} turns`\n : `${session.turns.length} turns`;\n\n const sessionInfo = [\n `Session: ${chalk.cyan(shortSessionId(session.id))}`,\n projectNameFromSlug(session.projectSlug),\n formatDuration(session.durationMs),\n session.model,\n agentInfo,\n ].join(chalk.dim(\" | \"));\n lines.push(` ${sessionInfo}`);\n lines.push(\"\");\n\n const gradeColor = getGradeColor(grade.letter);\n lines.push(` Overall grade: ${gradeColor(chalk.bold(grade.letter))}`);\n lines.push(\"\");\n\n const table = new Table({\n head: [\"Metric\", \"Value\", \"Status\"].map((h) => chalk.dim(h)),\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const metric of grade.metrics) {\n const displayName = METRIC_DISPLAY_NAMES[metric.name] ?? metric.name;\n const icon = STATUS_ICONS[metric.status] ?? \"\";\n table.push([displayName, metric.label, `${icon} ${STATUS_LABELS[metric.status] ?? metric.status}`]);\n }\n\n lines.push(table.toString());\n\n const tips = getAllTips(grade.metrics);\n if (tips.length > 0) {\n lines.push(\"\");\n lines.push(chalk.yellow(\" Tips:\"));\n for (const tip of tips) {\n lines.push(` ${chalk.dim(\"→\")} ${tip}`);\n }\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nexport function renderTrendReport(\n results: RegressionResult[],\n sessionCount: number,\n period: string,\n): string {\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(chalk.bold(` Trend report: last ${period}`) + chalk.dim(` (${sessionCount} sessions)`));\n lines.push(\"\");\n\n const table = new Table({\n head: [\"Metric\", \"Recent avg\", \"Full avg\", \"Change\", \"Status\"].map((h) => chalk.dim(h)),\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const result of results) {\n const displayName = METRIC_DISPLAY_NAMES[result.name] ?? result.name;\n const recentStr = result.recentAvg !== null ? result.recentAvg.toFixed(2) : \"N/A\";\n const fullStr = result.fullAvg !== null ? result.fullAvg.toFixed(2) : \"N/A\";\n\n let changeStr = \"N/A\";\n if (result.changePercent !== null) {\n const arrow = result.changePercent > 0 ? \"▲\" : result.changePercent < 0 ? \"▼\" : \"\";\n changeStr = `${arrow} ${Math.abs(Math.round(result.changePercent))}%`;\n }\n\n const statusStr = formatRegressionStatus(result.status);\n table.push([displayName, recentStr, fullStr, changeStr, statusStr]);\n }\n\n lines.push(table.toString());\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nexport function renderCacheCheckReport(results: CacheCheckResult[]): string {\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(chalk.bold(\" Cache health check\"));\n lines.push(\"\");\n\n const table = new Table({\n head: [\"Session\", \"Project\", \"Cache Hit\", \"Status\"].map((h) => chalk.dim(h)),\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const result of results) {\n const hitStr = result.cacheHitRate !== null ? result.cacheHitRate.toFixed(2) : \"N/A\";\n const statusStr = result.isAnomaly\n ? chalk.red(\"✗ ANOMALY\")\n : chalk.green(\"✓ normal\");\n\n table.push([\n shortSessionId(result.sessionId),\n projectNameFromSlug(result.projectSlug),\n hitStr,\n statusStr,\n ]);\n }\n\n lines.push(table.toString());\n\n const anomalies = results.filter((r) => r.isAnomaly);\n if (anomalies.length > 0) {\n lines.push(\"\");\n lines.push(\n chalk.yellow(` ⚠ ${anomalies.length} session(s) with abnormally low cache hit rate.`),\n );\n for (const a of anomalies) {\n if (a.estimatedInflation) {\n lines.push(\n ` ${chalk.dim(\"→\")} Session ${shortSessionId(a.sessionId)} consumed ~${a.estimatedInflation}x more input tokens than expected.`,\n );\n }\n }\n lines.push(\n chalk.dim(\" Try: restart Claude Code or downgrade to a previous version.\"),\n );\n } else {\n lines.push(\"\");\n lines.push(chalk.green(\" ✓ No cache anomalies detected.\"));\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\nfunction getGradeColor(letter: string): (text: string) => string {\n if (letter.startsWith(\"A\")) return chalk.green;\n if (letter.startsWith(\"B\")) return chalk.cyan;\n if (letter.startsWith(\"C\")) return chalk.yellow;\n return chalk.red;\n}\n\nfunction formatRegressionStatus(status: string): string {\n switch (status) {\n case \"stable\":\n return chalk.green(\"✓ stable\");\n case \"declining\":\n return chalk.yellow(\"⚠ declining\");\n case \"regression\":\n return chalk.red(\"⚠ REGRESSION\");\n default:\n return status;\n }\n}\n","/**\n * Number and string formatting helpers for terminal output.\n */\n\n/** Format a number with comma separators: 3218 → \"3,218\" */\nexport function formatNumber(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\n/** Format a ratio as a fixed-2 decimal: 0.734 → \"0.73\" */\nexport function formatRatio(n: number): string {\n return n.toFixed(2);\n}\n\n/** Format a percentage: 0.734 → \"73%\" */\nexport function formatPercent(n: number): string {\n return `${Math.round(n * 100)}%`;\n}\n\n/** Format milliseconds into a human-readable duration: 2820000 → \"47 min\" */\nexport function formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes} min`;\n\n const hours = Math.floor(minutes / 60);\n const remainingMinutes = minutes % 60;\n if (remainingMinutes === 0) return `${hours}h`;\n return `${hours}h ${remainingMinutes}m`;\n}\n\n/** Truncate a session ID for display: \"31f3f224-abcd-...\" → \"31f3f224\" */\nexport function shortSessionId(id: string): string {\n return id.slice(0, 8);\n}\n\n/** Extract a human-readable project name from a slug like \"-Users-foo-my-app\" */\nexport function projectNameFromSlug(slug: string): string {\n const parts = slug.split(\"-\").filter(Boolean);\n return parts[parts.length - 1] || slug;\n}\n","/**\n * Contextual tips based on metric values.\n * Maps poor-scoring metrics to actionable suggestions.\n */\n\nimport type { MetricResult } from \"../parser/types.js\";\n\nconst TIPS: Record<string, Record<string, string>> = {\n \"reads-per-edit\": {\n warning: \"Claude is editing with less context. Add 'Always read files before editing' to your CLAUDE.md.\",\n critical: \"Very low reads before edits. Claude is making blind changes. Consider adding explicit read instructions.\",\n },\n \"rewrite-ratio\": {\n warning: \"High ratio of full-file rewrites. Add 'Prefer Edit over Write for existing files' to CLAUDE.md.\",\n critical: \"Claude is rewriting entire files instead of making surgical edits. This wastes tokens and risks data loss.\",\n },\n \"cache-hit-rate\": {\n warning: \"Cache hit rate is below normal. Sessions may be too short for caching to help.\",\n critical: \"Very low cache hit rate — possible cache bug. Try restarting Claude Code or downgrading to a previous version.\",\n },\n \"task-completion\": {\n warning: \"Claude is occasionally promising actions without following through.\",\n critical: \"Frequent unfulfilled promises. Claude says it will do things but doesn't. Try breaking tasks into smaller steps.\",\n },\n \"retry-density\": {\n warning: \"Some user messages look like retries. Claude may be misunderstanding requests.\",\n critical: \"High retry rate. Users are frequently re-asking. Consider providing more context in prompts.\",\n },\n \"tool-diversity\": {\n warning: \"Low tool diversity. Claude is over-relying on a narrow set of tools.\",\n critical: \"Very narrow tool usage. Claude may be stuck in a pattern. Try prompting for specific tool usage.\",\n },\n \"tokens-per-edit\": {\n warning: \"Tokens per edit is above average. Claude may be verbose without being productive.\",\n critical: \"Very high token cost per edit. Claude is burning tokens without proportional output.\",\n },\n};\n\nexport function getTip(metric: MetricResult): string | null {\n if (metric.status === \"healthy\") return null;\n\n const metricTips = TIPS[metric.name];\n if (!metricTips) return null;\n\n return metricTips[metric.status] ?? null;\n}\n\nexport function getAllTips(metrics: MetricResult[]): string[] {\n return metrics\n .map(getTip)\n .filter((tip): tip is string => tip !== null);\n}\n","/**\n * JSON output mode for scripting and CI.\n */\n\nimport type { GradeResult, Session } from \"../parser/types.js\";\nimport type { RegressionResult } from \"../anomaly/regression-detector.js\";\nimport type { CacheCheckResult } from \"../anomaly/cache-anomaly.js\";\n\nexport interface AuditJsonOutput {\n session: {\n id: string;\n project: string;\n model: string;\n durationMs: number;\n startTime: string;\n };\n grade: string;\n score: number;\n metrics: Array<{\n name: string;\n value: number | null;\n status: string;\n label: string;\n }>;\n}\n\nexport function formatAuditJson(session: Session, grade: GradeResult): string {\n const output: AuditJsonOutput = {\n session: {\n id: session.id,\n project: session.projectSlug,\n model: session.model,\n durationMs: session.durationMs,\n startTime: session.startTime,\n },\n grade: grade.letter,\n score: grade.score,\n metrics: grade.metrics.map((m) => ({\n name: m.name,\n value: m.value,\n status: m.status,\n label: m.label,\n })),\n };\n\n return JSON.stringify(output, null, 2);\n}\n\nexport function formatTrendJson(results: RegressionResult[]): string {\n return JSON.stringify({ trend: results }, null, 2);\n}\n\nexport function formatCacheCheckJson(results: CacheCheckResult[]): string {\n return JSON.stringify({ cacheCheck: results }, null, 2);\n}\n","/**\n * Default command — grade the most recent session.\n */\n\nimport { getMostRecentSession } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { gradeSession } from \"../metrics/grader.js\";\nimport { renderAuditReport } from \"../reporter/terminal.js\";\nimport { formatAuditJson } from \"../reporter/json-reporter.js\";\n\nexport interface AuditOptions {\n json?: boolean;\n verbose?: boolean;\n dataDir?: string;\n project?: string;\n}\n\nexport async function runAudit(options: AuditOptions): Promise<void> {\n const sessionFile = await getMostRecentSession({\n dataDir: options.dataDir,\n project: options.project,\n });\n\n const records = readJsonl(sessionFile.path);\n const session = await buildSession(\n records,\n sessionFile.sessionId,\n sessionFile.projectSlug,\n sessionFile.subagentPaths,\n );\n\n const grade = gradeSession(session);\n\n if (options.json) {\n console.log(formatAuditJson(session, grade));\n } else {\n console.log(renderAuditReport(session, grade));\n }\n}\n","/**\n * Compute rolling averages from multiple sessions for trend analysis.\n */\n\nimport type { GradeResult } from \"../parser/types.js\";\n\nexport interface MetricAverage {\n name: string;\n recentAvg: number | null;\n fullAvg: number | null;\n changePercent: number | null;\n}\n\n/**\n * Compute per-metric averages for a recent window vs. full range.\n * @param grades - All graded sessions, sorted most recent first\n * @param recentCount - Number of sessions in the \"recent\" window\n */\nexport function computeBaselines(\n grades: GradeResult[],\n recentCount: number,\n): MetricAverage[] {\n if (grades.length === 0) return [];\n\n const recent = grades.slice(0, recentCount);\n const full = grades;\n\n const metricNames = grades[0].metrics.map((m) => m.name);\n\n return metricNames.map((name) => {\n const recentValues = extractValues(recent, name);\n const fullValues = extractValues(full, name);\n\n const recentAvg = average(recentValues);\n const fullAvg = average(fullValues);\n\n let changePercent: number | null = null;\n if (recentAvg !== null && fullAvg !== null && fullAvg !== 0) {\n changePercent = ((recentAvg - fullAvg) / Math.abs(fullAvg)) * 100;\n }\n\n return { name, recentAvg, fullAvg, changePercent };\n });\n}\n\nfunction extractValues(grades: GradeResult[], metricName: string): number[] {\n const values: number[] = [];\n for (const grade of grades) {\n const metric = grade.metrics.find((m) => m.name === metricName);\n if (metric?.value !== null && metric?.value !== undefined) {\n values.push(metric.value);\n }\n }\n return values;\n}\n\nfunction average(values: number[]): number | null {\n if (values.length === 0) return null;\n return values.reduce((a, b) => a + b, 0) / values.length;\n}\n","/**\n * Z-score based regression detection.\n *\n * Compares recent metric values against historical baseline to flag\n * statistically significant regressions.\n */\n\nimport type { MetricAverage } from \"./baseline.js\";\n\nexport type RegressionStatus = \"stable\" | \"declining\" | \"regression\";\n\nexport interface RegressionResult {\n name: string;\n recentAvg: number | null;\n fullAvg: number | null;\n changePercent: number | null;\n status: RegressionStatus;\n}\n\n/** Metrics where HIGHER values are WORSE (inverted for regression detection). */\nconst INVERTED_METRICS = new Set([\n \"rewrite-ratio\",\n \"retry-density\",\n \"tokens-per-edit\",\n]);\n\n/**\n * Detect regressions from baseline averages.\n * A change > 30% in the \"bad\" direction is a regression.\n * A change > 10% is \"declining\".\n */\nexport function detectRegressions(\n baselines: MetricAverage[],\n): RegressionResult[] {\n return baselines.map((b) => {\n let status: RegressionStatus = \"stable\";\n\n if (b.changePercent !== null) {\n const isInverted = INVERTED_METRICS.has(b.name);\n // For normal metrics, negative change is bad. For inverted, positive change is bad.\n const badDirection = isInverted ? b.changePercent > 0 : b.changePercent < 0;\n const magnitude = Math.abs(b.changePercent);\n\n if (badDirection && magnitude > 30) {\n status = \"regression\";\n } else if (badDirection && magnitude > 10) {\n status = \"declining\";\n }\n }\n\n return {\n name: b.name,\n recentAvg: b.recentAvg,\n fullAvg: b.fullAvg,\n changePercent: b.changePercent,\n status,\n };\n });\n}\n","/**\n * Parse human-readable duration strings into Date offsets.\n */\n\n/**\n * Parse a duration string like \"7d\", \"14d\", \"30d\" into a Date\n * representing that many days before `now`.\n */\nexport function parseDuration(duration: string, now = new Date()): Date {\n const match = duration.match(/^(\\d+)d$/);\n if (!match) {\n throw new Error(\n `Invalid duration: \"${duration}\". Use format like \"7d\", \"14d\", \"30d\".`,\n );\n }\n\n const days = parseInt(match[1], 10);\n const result = new Date(now);\n result.setDate(result.getDate() - days);\n return result;\n}\n","/**\n * Trend analysis command — detect regressions over time.\n */\n\nimport { scanSessions } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { gradeSession } from \"../metrics/grader.js\";\nimport { computeBaselines } from \"../anomaly/baseline.js\";\nimport { detectRegressions } from \"../anomaly/regression-detector.js\";\nimport { renderTrendReport } from \"../reporter/terminal.js\";\nimport { formatTrendJson } from \"../reporter/json-reporter.js\";\nimport { parseDuration } from \"../utils/duration.js\";\nimport type { GradeResult } from \"../parser/types.js\";\n\nexport interface TrendOptions {\n since?: string;\n json?: boolean;\n dataDir?: string;\n project?: string;\n}\n\nexport async function runTrend(options: TrendOptions): Promise<void> {\n const duration = options.since ?? \"7d\";\n const sinceDate = parseDuration(duration);\n\n const sessionFiles = await scanSessions({\n dataDir: options.dataDir,\n project: options.project,\n since: sinceDate,\n });\n\n if (sessionFiles.length === 0) {\n console.log(`No sessions found in the last ${duration}.`);\n return;\n }\n\n const settled = await Promise.allSettled(\n sessionFiles.map(async (sf) => {\n const records = readJsonl(sf.path);\n const session = await buildSession(records, sf.sessionId, sf.projectSlug, sf.subagentPaths);\n return gradeSession(session);\n }),\n );\n const grades: GradeResult[] = settled\n .filter((r): r is PromiseFulfilledResult<GradeResult> => r.status === \"fulfilled\")\n .map((r) => r.value);\n\n if (grades.length === 0) {\n console.log(\"No valid sessions found to analyze.\");\n return;\n }\n\n // Use half the sessions as the \"recent\" window, minimum 1\n const recentCount = Math.max(1, Math.floor(grades.length / 2));\n const baselines = computeBaselines(grades, recentCount);\n const regressions = detectRegressions(baselines);\n\n if (options.json) {\n console.log(formatTrendJson(regressions));\n } else {\n console.log(renderTrendReport(regressions, grades.length, duration));\n }\n}\n","/**\n * Cache hit rate anomaly detection.\n *\n * Specifically checks for the prompt cache bug that caused 10-20x\n * token cost inflation by detecting sessions with near-zero cache hit rates.\n */\n\nimport type { Session } from \"../parser/types.js\";\nimport { computeCacheHitRate } from \"../metrics/cache-hit-rate.js\";\n\nexport interface CacheCheckResult {\n sessionId: string;\n projectSlug: string;\n timestamp: string;\n cacheHitRate: number | null;\n isAnomaly: boolean;\n estimatedInflation: number | null;\n}\n\nconst ANOMALY_THRESHOLD = 0.05;\nconst NORMAL_CACHE_RATE = 0.65;\n\n/**\n * Check a single session for cache hit rate anomalies.\n */\nexport function checkCacheAnomaly(session: Session): CacheCheckResult {\n const metric = computeCacheHitRate(session);\n\n const isAnomaly = metric.value !== null && metric.value < ANOMALY_THRESHOLD;\n\n let estimatedInflation: number | null = null;\n if (isAnomaly && metric.value !== null) {\n // If normal rate is 65% cache reads, the effective input cost multiplier\n // when cache is broken is roughly 1 / (1 - normalRate)\n // Normal: 35% full-price tokens. Broken: 100% full-price tokens.\n estimatedInflation = Math.round(1 / (1 - NORMAL_CACHE_RATE));\n }\n\n return {\n sessionId: session.id,\n projectSlug: session.projectSlug,\n timestamp: session.startTime,\n cacheHitRate: metric.value,\n isAnomaly,\n estimatedInflation,\n };\n}\n","/**\n * Cache bug detection command.\n * Scans recent sessions for abnormally low cache hit rates.\n */\n\nimport { scanSessions } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { checkCacheAnomaly } from \"../anomaly/cache-anomaly.js\";\nimport { renderCacheCheckReport } from \"../reporter/terminal.js\";\nimport { formatCacheCheckJson } from \"../reporter/json-reporter.js\";\nimport { parseDuration } from \"../utils/duration.js\";\nimport type { CacheCheckResult } from \"../anomaly/cache-anomaly.js\";\n\nexport interface CacheCheckOptions {\n since?: string;\n json?: boolean;\n dataDir?: string;\n}\n\nexport async function runCacheCheck(options: CacheCheckOptions): Promise<void> {\n const duration = options.since ?? \"7d\";\n const sinceDate = parseDuration(duration);\n\n const sessionFiles = await scanSessions({\n dataDir: options.dataDir,\n since: sinceDate,\n });\n\n if (sessionFiles.length === 0) {\n console.log(`No sessions found in the last ${duration}.`);\n return;\n }\n\n const settled = await Promise.allSettled(\n sessionFiles.map(async (sf) => {\n const records = readJsonl(sf.path);\n const session = await buildSession(records, sf.sessionId, sf.projectSlug, sf.subagentPaths);\n return checkCacheAnomaly(session);\n }),\n );\n const results: CacheCheckResult[] = settled\n .filter((r): r is PromiseFulfilledResult<CacheCheckResult> => r.status === \"fulfilled\")\n .map((r) => r.value);\n\n if (results.length === 0) {\n console.log(\"No valid sessions found to analyze.\");\n return;\n }\n\n if (options.json) {\n console.log(formatCacheCheckJson(results));\n } else {\n console.log(renderCacheCheckReport(results));\n }\n}\n","/**\n * Cross-project comparison command.\n * Compares average quality metrics across multiple projects.\n */\n\nimport { scanSessions } from \"../parser/project-scanner.js\";\nimport { readJsonl } from \"../parser/jsonl-reader.js\";\nimport { buildSession } from \"../parser/session-builder.js\";\nimport { gradeSession } from \"../metrics/grader.js\";\nimport chalk from \"chalk\";\nimport Table from \"cli-table3\";\nimport type { GradeResult } from \"../parser/types.js\";\nimport { projectNameFromSlug } from \"../utils/format.js\";\n\nexport interface CompareOptions {\n projects: string;\n json?: boolean;\n dataDir?: string;\n since?: string;\n}\n\ninterface ProjectSummary {\n name: string;\n sessionCount: number;\n avgGrade: number;\n avgLetter: string;\n metrics: Map<string, number>;\n}\n\nexport async function runCompare(options: CompareOptions): Promise<void> {\n const projectNames = options.projects.split(\",\").map((p) => p.trim());\n const summaries: ProjectSummary[] = [];\n\n const projectSummaries = await Promise.all(\n projectNames.map(async (projectFilter) => {\n const sessionFiles = await scanSessions({\n dataDir: options.dataDir,\n project: projectFilter,\n });\n\n if (sessionFiles.length === 0) return null;\n\n const settled = await Promise.allSettled(\n sessionFiles.map(async (sf) => {\n const records = readJsonl(sf.path);\n const session = await buildSession(records, sf.sessionId, sf.projectSlug, sf.subagentPaths);\n return gradeSession(session);\n }),\n );\n const grades: GradeResult[] = settled\n .filter((r): r is PromiseFulfilledResult<GradeResult> => r.status === \"fulfilled\")\n .map((r) => r.value);\n\n if (grades.length === 0) return null;\n\n const avgScore = grades.reduce((s, g) => s + g.score, 0) / grades.length;\n const metricAvgs = new Map<string, number>();\n for (const metric of grades[0].metrics) {\n const values = grades\n .map((g) => g.metrics.find((m) => m.name === metric.name)?.value)\n .filter((v): v is number => v !== null);\n if (values.length > 0) {\n metricAvgs.set(metric.name, values.reduce((a, b) => a + b, 0) / values.length);\n }\n }\n\n return {\n name: projectFilter,\n sessionCount: grades.length,\n avgGrade: Math.round(avgScore),\n avgLetter: getLetterGrade(avgScore),\n metrics: metricAvgs,\n } as ProjectSummary;\n }),\n );\n\n for (const s of projectSummaries) {\n if (s !== null) summaries.push(s);\n }\n\n if (summaries.length === 0) {\n console.log(\"No matching projects found.\");\n return;\n }\n\n if (options.json) {\n const jsonOutput = summaries.map((s) => ({\n project: s.name,\n sessions: s.sessionCount,\n grade: s.avgLetter,\n score: s.avgGrade,\n metrics: Object.fromEntries(s.metrics),\n }));\n console.log(JSON.stringify({ compare: jsonOutput }, null, 2));\n return;\n }\n\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(chalk.bold(\" Project comparison\"));\n lines.push(\"\");\n\n const head = [\"Project\", \"Sessions\", \"Grade\", ...summaries[0]?.metrics.keys() ?? []].map(\n (h) => chalk.dim(h),\n );\n\n const table = new Table({\n head,\n style: { head: [], border: [], \"padding-left\": 2, \"padding-right\": 2 },\n chars: {\n top: \"─\", \"top-mid\": \"─\", \"top-left\": \" \", \"top-right\": \"\",\n bottom: \"─\", \"bottom-mid\": \"─\", \"bottom-left\": \" \", \"bottom-right\": \"\",\n left: \" \", \"left-mid\": \" \", mid: \"─\", \"mid-mid\": \"─\",\n right: \"\", \"right-mid\": \"\", middle: \" \",\n },\n });\n\n for (const summary of summaries) {\n const row: string[] = [\n summary.name,\n summary.sessionCount.toString(),\n summary.avgLetter,\n ];\n for (const [, value] of summary.metrics) {\n row.push(value.toFixed(2));\n }\n table.push(row);\n }\n\n lines.push(table.toString());\n lines.push(\"\");\n console.log(lines.join(\"\\n\"));\n}\n\nfunction getLetterGrade(score: number): string {\n if (score >= 97) return \"A+\";\n if (score >= 93) return \"A\";\n if (score >= 90) return \"A-\";\n if (score >= 87) return \"B+\";\n if (score >= 83) return \"B\";\n if (score >= 80) return \"B-\";\n if (score >= 77) return \"C+\";\n if (score >= 73) return \"C\";\n if (score >= 70) return \"C-\";\n if (score >= 67) return \"D+\";\n if (score >= 63) return \"D\";\n if (score >= 60) return \"D-\";\n return \"F\";\n}\n"],"mappings":"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","names":["join","join","basename","basename","average","round","round","round","round","round","EDIT_TOOLS","chalk","Table"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inspecto",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "inspecto — Claude Code session quality analyzer. Grade sessions, detect regressions, catch cache bugs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",