@echomem/mcp 1.4.7 → 1.4.9
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 +35 -9
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/chaos-to-clarity-pencil.html +582 -0
- package/dist/city/echo-ai-city-only.html +1126 -109
- package/dist/city/echo-ai-city-only.template.html +1126 -109
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/pencil-pie-generator.html +883 -0
- package/dist/city/pencil-webgl-landscape.html +1239 -0
- package/dist/city/spatial-fan-story.html +479 -0
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-native-canonical.js +1193 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +56 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +8 -4
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/metric.js +13 -4
- package/dist/hud/monitor.js +171 -84
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +880 -270
- package/dist/index.js +122 -24
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +55 -29
- package/dist/report.js +101 -40
- package/dist/setup-page.js +4257 -245
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +786 -75
- package/dist/v1-contract.js +20 -2
- package/package.json +6 -4
- package/templates/echomem-recall.md +2 -2
|
@@ -0,0 +1,1628 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SESSION_ID = "019f19e7-9e7a-72a3-86ad-bf7c6bcc6ab1";
|
|
6
|
+
const DEFAULT_DATA_DIR = "ErikMachine-Context_Golden_Standard/data";
|
|
7
|
+
const DEFAULT_REPORT_DIR = "ErikMachine-Context_Golden_Standard/reports";
|
|
8
|
+
const DEFAULT_EFFICIENCY = path.join(DEFAULT_DATA_DIR, "OPTIMIZABLE_DETAIL_WebPageReactVersion_77t.json");
|
|
9
|
+
const USEFUL_COLOR = "#3baa75";
|
|
10
|
+
const UNATTRIBUTED_WASTE = {
|
|
11
|
+
id: "unattributed",
|
|
12
|
+
name: "Unlabeled outcome residue",
|
|
13
|
+
color: "#b7bdc8"
|
|
14
|
+
};
|
|
15
|
+
const PROBLEM_COLORS = {
|
|
16
|
+
P01: "#4c78a8",
|
|
17
|
+
P02: "#f58518",
|
|
18
|
+
P03: "#c06c2f",
|
|
19
|
+
P04: "#7b61b9",
|
|
20
|
+
P05: "#d45087",
|
|
21
|
+
P06: "#8c6d62",
|
|
22
|
+
P07: "#e45756",
|
|
23
|
+
P08: "#2cb1bc",
|
|
24
|
+
P09: "#72b01d",
|
|
25
|
+
P10: "#c17db4",
|
|
26
|
+
P11: "#f2c14e",
|
|
27
|
+
P12: "#00a878",
|
|
28
|
+
P13: "#5b6cff"
|
|
29
|
+
};
|
|
30
|
+
const PROBLEM_DEFINITIONS = {
|
|
31
|
+
P01: "Too many screenshots or images remain in context after they stop being the active reference.",
|
|
32
|
+
P02: "The model had a known user constraint or correction signal, but the work still violated it and required repair.",
|
|
33
|
+
P03: "Old file reads, duplicate file versions, or superseded patch drafts accumulate beside the current version.",
|
|
34
|
+
P04: "After compaction, the model has to rediscover files, searches, or git state that it had already found.",
|
|
35
|
+
P05: "The assistant treated work as done, but the user corrected it, so the next turn pays repair debt.",
|
|
36
|
+
P06: "After a time gap or state gap, the model's working view no longer matches the repo or task state.",
|
|
37
|
+
P07: "A long or aborted turn may leave file, process, or disk side effects that later turns must recover from.",
|
|
38
|
+
P08: "Git status, git diff, or related repo-state commands are repeated without adding enough new information.",
|
|
39
|
+
P09: "The execution path drifts and needs repeated correction, retry, validation, or reorientation.",
|
|
40
|
+
P10: "The model repeats the same search, query, or memory lookup instead of reusing prior knowledge.",
|
|
41
|
+
P11: "Browser/visual inspection outputs, DOM dumps, route checks, and viewport/debug metadata remain in context after serving visual QA.",
|
|
42
|
+
P12: "Tool, command, search, file-read, memory lookup, and orientation logs remain in context after they stop being operationally useful.",
|
|
43
|
+
P13: "Agent explanations, reconciliation notes, planning residue, and conversation/reasoning-adjacent text remain in context after they stop being operationally useful."
|
|
44
|
+
};
|
|
45
|
+
const DIRECT_PROBLEM_PRIORITY = ["P07", "P02", "P08", "P10", "P06", "P04", "P05", "P03", "P09", "P01"];
|
|
46
|
+
const ADDITIONAL_PROBLEMS = [
|
|
47
|
+
{ id: "P11", name: "Visual Debug Logs", category: "Context Hygiene" },
|
|
48
|
+
{ id: "P12", name: "Tool Call Logs", category: "Context Hygiene" },
|
|
49
|
+
{ id: "P13", name: "Agent's Reasoning Notes", category: "Context Hygiene" }
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const args = parseArgs(process.argv.slice(2));
|
|
53
|
+
const sessionId = args["session-id"] || args.session || args._[0] || DEFAULT_SESSION_ID;
|
|
54
|
+
const problemPath = path.resolve(
|
|
55
|
+
args.problems || path.join(DEFAULT_DATA_DIR, `TEN_PROBLEM_ANALYSIS_${sessionId}.json`)
|
|
56
|
+
);
|
|
57
|
+
const efficiencyPath = path.resolve(args.efficiency || DEFAULT_EFFICIENCY);
|
|
58
|
+
const outPath = path.resolve(
|
|
59
|
+
args.out || path.join(DEFAULT_DATA_DIR, `SESSION_WASTE_DASHBOARD_${sessionId}.json`)
|
|
60
|
+
);
|
|
61
|
+
const htmlPath = path.resolve(
|
|
62
|
+
args.html || path.join(DEFAULT_REPORT_DIR, `SESSION_WASTE_DASHBOARD_${sessionId}.html`)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const problemReport = readJson(problemPath);
|
|
66
|
+
const efficiencyReport = readJson(efficiencyPath);
|
|
67
|
+
const dashboard = buildDashboard({ sessionId, problemPath, efficiencyPath, problemReport, efficiencyReport });
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
70
|
+
fs.writeFileSync(outPath, `${JSON.stringify(dashboard, null, 2)}\n`);
|
|
71
|
+
if (process.env.GOLDEN_JSON_ONLY !== "1") {
|
|
72
|
+
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
|
73
|
+
fs.writeFileSync(htmlPath, renderHtml(dashboard));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log("Session waste dashboard complete");
|
|
77
|
+
console.log(` Session: ${sessionId}`);
|
|
78
|
+
console.log(` Total input: ${fmt(dashboard.totals.officialInputTokens)}`);
|
|
79
|
+
console.log(` Useful: ${fmt(dashboard.totals.usefulTokens)} (${pct(dashboard.totals.usefulSharePct)})`);
|
|
80
|
+
console.log(` Waste: ${fmt(dashboard.totals.wasteTokens)} (${pct(dashboard.totals.wasteSharePct)})`);
|
|
81
|
+
console.log(` Major contributor: ${dashboard.majorContributor.byAllocatedWaste.id} ${dashboard.majorContributor.byAllocatedWaste.name}`);
|
|
82
|
+
console.log(` JSON: ${outPath}`);
|
|
83
|
+
if (process.env.GOLDEN_JSON_ONLY !== "1") console.log(` HTML: ${htmlPath}`);
|
|
84
|
+
|
|
85
|
+
function buildDashboard({ sessionId, problemPath, efficiencyPath, problemReport, efficiencyReport }) {
|
|
86
|
+
const efficiencyRows = efficiencyReport.series || [];
|
|
87
|
+
const turnMetaByNumber = new Map((problemReport.turns || []).map((turn) => [turn.number, turn]));
|
|
88
|
+
const allProblems = [...(problemReport.problems || []), ...ADDITIONAL_PROBLEMS];
|
|
89
|
+
const problemMetaById = new Map(allProblems.map((problem) => [problem.id, problem]));
|
|
90
|
+
const problemByTurn = new Map();
|
|
91
|
+
|
|
92
|
+
for (const problem of problemReport.problems || []) {
|
|
93
|
+
for (const turn of problem.qualifiedTurns || []) {
|
|
94
|
+
const list = problemByTurn.get(turn) || [];
|
|
95
|
+
list.push(problem.id);
|
|
96
|
+
problemByTurn.set(turn, list);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const contributions = allProblems.map((problem) => ({
|
|
101
|
+
id: problem.id,
|
|
102
|
+
name: problem.name,
|
|
103
|
+
category: problem.category,
|
|
104
|
+
definition: PROBLEM_DEFINITIONS[problem.id] || "",
|
|
105
|
+
color: PROBLEM_COLORS[problem.id] || "#475467",
|
|
106
|
+
eventsFound: problem.eventsFound,
|
|
107
|
+
evidenceTurns: problem.turns?.length || 0,
|
|
108
|
+
qualifiedTurns: problem.qualifiedTurns?.length || 0,
|
|
109
|
+
tokenPressure: problem.tokenPressure || 0,
|
|
110
|
+
qualifiedTokenPressure: problem.qualifiedTokenPressure || 0,
|
|
111
|
+
allocatedWasteTokens: 0,
|
|
112
|
+
allocatedWasteByBucket: {
|
|
113
|
+
duplicateTokens: 0,
|
|
114
|
+
refindTokens: 0,
|
|
115
|
+
deadTokens: 0
|
|
116
|
+
},
|
|
117
|
+
_turnWasteTokensByTurn: {},
|
|
118
|
+
worstTurnByAllocatedWaste: null,
|
|
119
|
+
worstTurnByQualifiedPressure: problem.worstTurn || null
|
|
120
|
+
}));
|
|
121
|
+
const contributionById = new Map(contributions.map((problem) => [problem.id, problem]));
|
|
122
|
+
|
|
123
|
+
let wasteCoveredByProblems = 0;
|
|
124
|
+
let unlabeledResidueTokens = 0;
|
|
125
|
+
let overheadUsefulTokens = 0;
|
|
126
|
+
const timeline = [];
|
|
127
|
+
|
|
128
|
+
for (const row of efficiencyRows) {
|
|
129
|
+
const turnMeta = turnMetaByNumber.get(row.turn) || {};
|
|
130
|
+
const turnProblems = problemByTurn.get(row.turn) || [];
|
|
131
|
+
const overheadTokens = row.keep_oh || 0;
|
|
132
|
+
const confidentlyUsefulTokens = row.keep_prod || 0;
|
|
133
|
+
const rawUsefulTokens = overheadTokens + confidentlyUsefulTokens;
|
|
134
|
+
const rawOutcomeWasteTokens = (row.opt_dup || 0) + (row.opt_refind || 0) + (row.opt_dead || 0);
|
|
135
|
+
const officialInputTokens = row.C_t || rawUsefulTokens + rawOutcomeWasteTokens;
|
|
136
|
+
const allocation = allocateTurnWaste({
|
|
137
|
+
row,
|
|
138
|
+
turnProblems,
|
|
139
|
+
wasteTokens: rawOutcomeWasteTokens,
|
|
140
|
+
problemMetaById,
|
|
141
|
+
problemByTurn
|
|
142
|
+
});
|
|
143
|
+
const labeledWasteTokens = allocation.coveredWasteTokens;
|
|
144
|
+
const usefulTokens = rawUsefulTokens;
|
|
145
|
+
const wasteTokens = rawOutcomeWasteTokens;
|
|
146
|
+
const classifiedInputTokens = confidentlyUsefulTokens + labeledWasteTokens;
|
|
147
|
+
const namedBuckets = sumLabeledBuckets(allocation.problemItems);
|
|
148
|
+
|
|
149
|
+
wasteCoveredByProblems += labeledWasteTokens;
|
|
150
|
+
unlabeledResidueTokens += allocation.unattributedWasteTokens;
|
|
151
|
+
overheadUsefulTokens += overheadTokens;
|
|
152
|
+
|
|
153
|
+
for (const item of allocation.problemItems) {
|
|
154
|
+
const contribution = contributionById.get(item.problemId);
|
|
155
|
+
if (!contribution) continue;
|
|
156
|
+
contribution.allocatedWasteTokens += item.tokens;
|
|
157
|
+
contribution.allocatedWasteByBucket.duplicateTokens += item.bucket === "opt_dup" ? item.tokens : 0;
|
|
158
|
+
contribution.allocatedWasteByBucket.refindTokens += item.bucket === "opt_refind" ? item.tokens : 0;
|
|
159
|
+
contribution.allocatedWasteByBucket.deadTokens += item.bucket === "opt_dead" ? item.tokens : 0;
|
|
160
|
+
const turnKey = String(row.turn);
|
|
161
|
+
const turnWaste = (contribution._turnWasteTokensByTurn[turnKey] || 0) + item.tokens;
|
|
162
|
+
contribution._turnWasteTokensByTurn[turnKey] = turnWaste;
|
|
163
|
+
if (!contribution.worstTurnByAllocatedWaste || turnWaste > contribution.worstTurnByAllocatedWaste.allocatedWasteTokens) {
|
|
164
|
+
contribution.worstTurnByAllocatedWaste = {
|
|
165
|
+
turn: row.turn,
|
|
166
|
+
episode: row.episode,
|
|
167
|
+
inputTokens: officialInputTokens,
|
|
168
|
+
usefulTokens,
|
|
169
|
+
wasteTokens,
|
|
170
|
+
usefulSharePct: officialInputTokens ? (usefulTokens / officialInputTokens) * 100 : 0,
|
|
171
|
+
wasteSharePct: officialInputTokens ? (wasteTokens / officialInputTokens) * 100 : 0,
|
|
172
|
+
allocatedWasteTokens: turnWaste,
|
|
173
|
+
allocationMethod: allocation.method,
|
|
174
|
+
assignmentBasis: item.assignmentBasis,
|
|
175
|
+
sourceTurn: item.sourceTurn ?? row.turn,
|
|
176
|
+
itemKind: item.kind,
|
|
177
|
+
itemReason: item.reason
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (allocation.method === "turn_equal_split") {
|
|
183
|
+
for (const problemId of turnProblems) {
|
|
184
|
+
const contribution = contributionById.get(problemId);
|
|
185
|
+
if (!contribution) continue;
|
|
186
|
+
const fallbackItem = allocation.problemItems.find((item) => item.problemId === problemId);
|
|
187
|
+
if (!fallbackItem) continue;
|
|
188
|
+
contribution.allocatedWasteByBucket.duplicateTokens += (row.opt_dup || 0) / turnProblems.length;
|
|
189
|
+
contribution.allocatedWasteByBucket.refindTokens += (row.opt_refind || 0) / turnProblems.length;
|
|
190
|
+
contribution.allocatedWasteByBucket.deadTokens += (row.opt_dead || 0) / turnProblems.length;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
timeline.push({
|
|
195
|
+
turn: row.turn,
|
|
196
|
+
episode: row.episode,
|
|
197
|
+
inputTokens: officialInputTokens,
|
|
198
|
+
officialInputTokens,
|
|
199
|
+
classifiedInputTokens,
|
|
200
|
+
userMessage: turnMeta.messagePreview || "",
|
|
201
|
+
timestamp: turnMeta.timestamp || null,
|
|
202
|
+
usefulTokens,
|
|
203
|
+
wasteTokens,
|
|
204
|
+
usefulSharePct: officialInputTokens ? (usefulTokens / officialInputTokens) * 100 : 0,
|
|
205
|
+
wasteSharePct: officialInputTokens ? (wasteTokens / officialInputTokens) * 100 : 0,
|
|
206
|
+
duplicateTokens: Math.round(row.opt_dup || 0),
|
|
207
|
+
refindTokens: Math.round(row.opt_refind || 0),
|
|
208
|
+
deadTokens: Math.round(row.opt_dead || 0),
|
|
209
|
+
namedWasteTokens: labeledWasteTokens,
|
|
210
|
+
unlabeledResidueTokens: allocation.unattributedWasteTokens,
|
|
211
|
+
productUsefulTokens: confidentlyUsefulTokens,
|
|
212
|
+
overheadUsefulTokens: overheadTokens,
|
|
213
|
+
namedDuplicateTokens: namedBuckets.duplicateTokens,
|
|
214
|
+
namedRefindTokens: namedBuckets.refindTokens,
|
|
215
|
+
namedDeadTokens: namedBuckets.deadTokens,
|
|
216
|
+
rawUsefulTokens,
|
|
217
|
+
rawOutcomeWasteTokens,
|
|
218
|
+
problems: allocation.problemIds,
|
|
219
|
+
detectedProblems: turnProblems,
|
|
220
|
+
problemCount: allocation.problemIds.length,
|
|
221
|
+
detectedProblemCount: turnProblems.length,
|
|
222
|
+
allocationMethod: allocation.method,
|
|
223
|
+
unattributedWasteTokens: allocation.unattributedWasteTokens,
|
|
224
|
+
allocatedItems: compactTimelineItems(allocation.problemItems, row.turn),
|
|
225
|
+
baselineItems: compactTimelineItems(allocation.unattributedItems, row.turn),
|
|
226
|
+
segments: buildTimelineSegments({
|
|
227
|
+
exactTotal: officialInputTokens,
|
|
228
|
+
usefulTokens,
|
|
229
|
+
wasteTokens,
|
|
230
|
+
allocation,
|
|
231
|
+
problemMetaById
|
|
232
|
+
}),
|
|
233
|
+
compaction: Boolean(turnMeta.compaction),
|
|
234
|
+
abort: Boolean(turnMeta.abort),
|
|
235
|
+
rollback: Boolean(turnMeta.rollback),
|
|
236
|
+
commit: Boolean(row.commit),
|
|
237
|
+
anchorKind: row.anchorKind || null
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const totals = sumDashboardTimeline(timeline);
|
|
242
|
+
const officialTotal = problemReport.totals?.officialInputTokens || totals.officialInputTokens;
|
|
243
|
+
const classifiedTotal = totals.classifiedInputTokens;
|
|
244
|
+
const tokenPressureTotal = sum(contributions, (problem) => problem.qualifiedTokenPressure);
|
|
245
|
+
|
|
246
|
+
for (const contribution of contributions) {
|
|
247
|
+
contribution.allocatedWasteTokens = Math.round(contribution.allocatedWasteTokens);
|
|
248
|
+
contribution.allocatedWasteByBucket = mapValues(contribution.allocatedWasteByBucket, Math.round);
|
|
249
|
+
contribution.allocatedWasteShareOfTotalPct = totals.wasteTokens
|
|
250
|
+
? round((contribution.allocatedWasteTokens / totals.wasteTokens) * 100, 1)
|
|
251
|
+
: 0;
|
|
252
|
+
contribution.allocatedWasteShareOfCoveredPct = wasteCoveredByProblems
|
|
253
|
+
? round((contribution.allocatedWasteTokens / wasteCoveredByProblems) * 100, 1)
|
|
254
|
+
: 0;
|
|
255
|
+
contribution.qualifiedPressureSharePct = tokenPressureTotal
|
|
256
|
+
? round((contribution.qualifiedTokenPressure / tokenPressureTotal) * 100, 1)
|
|
257
|
+
: 0;
|
|
258
|
+
contribution.qualifiedPressureVsSessionPct = officialTotal
|
|
259
|
+
? round((contribution.qualifiedTokenPressure / officialTotal) * 100, 1)
|
|
260
|
+
: 0;
|
|
261
|
+
if (contribution.worstTurnByAllocatedWaste) {
|
|
262
|
+
contribution.worstTurnByAllocatedWaste.allocatedWasteTokens = Math.round(
|
|
263
|
+
contribution.worstTurnByAllocatedWaste.allocatedWasteTokens
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
delete contribution._turnWasteTokensByTurn;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const sortedByAllocatedWaste = [...contributions].sort((a, b) => b.allocatedWasteTokens - a.allocatedWasteTokens);
|
|
270
|
+
const sortedByQualifiedPressure = [...contributions].sort((a, b) => b.qualifiedTokenPressure - a.qualifiedTokenPressure);
|
|
271
|
+
const episodeSummaries = buildEpisodeSummaries(timeline, problemReport.episodes || []);
|
|
272
|
+
const timelineTurnNumbers = new Set(timeline.map((turn) => turn.turn));
|
|
273
|
+
const keyDataPoints = buildKeyDataPoints({
|
|
274
|
+
problemReport,
|
|
275
|
+
timeline,
|
|
276
|
+
episodeSummaries,
|
|
277
|
+
totals,
|
|
278
|
+
sortedByAllocatedWaste
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
schemaVersion: "0.1.0",
|
|
283
|
+
generatedAt: new Date().toISOString(),
|
|
284
|
+
session: {
|
|
285
|
+
id: sessionId,
|
|
286
|
+
sourceSession: problemReport.session,
|
|
287
|
+
efficiencySource: efficiencyReport.session,
|
|
288
|
+
turnsInEfficiency: efficiencyRows.length,
|
|
289
|
+
turnsInProblemReport: problemReport.session?.turns || null,
|
|
290
|
+
tokenCountedTurns: problemReport.session?.tokenCountedTurns || null,
|
|
291
|
+
zeroInputTurnsExcluded: (problemReport.turns || [])
|
|
292
|
+
.filter((turn) => !timelineTurnNumbers.has(turn.number))
|
|
293
|
+
.map((turn) => ({
|
|
294
|
+
turn: turn.number,
|
|
295
|
+
inputTokens: turn.inputTokens || 0,
|
|
296
|
+
abort: Boolean(turn.abort),
|
|
297
|
+
rollback: Boolean(turn.rollback),
|
|
298
|
+
messagePreview: turn.messagePreview || ""
|
|
299
|
+
}))
|
|
300
|
+
},
|
|
301
|
+
inputs: {
|
|
302
|
+
problemPath,
|
|
303
|
+
efficiencyPath
|
|
304
|
+
},
|
|
305
|
+
method: {
|
|
306
|
+
wasteDefinition: "The headline denominator is the full official JSONL input token count for each turn. Useful includes required overhead plus product/task payload content. Waste includes all OPTIMIZABLE_DETAIL duplicate, re-find, and dead-work residue; named P-problem waste is shown as a breakdown, and unassigned residue is shown as Unlabeled outcome residue.",
|
|
307
|
+
problemAttribution: "Waste is attributed at item level when OPTIMIZABLE_DETAIL provides wasteItems. Direct matching uses the current turn's qualified P-problems; correction/drift-style labels P02, P05, and P09 only claim current or immediately adjacent repair residue rather than all carried context. Image-bearing function/tool outputs can source-causally claim P01; carried git command residue can source-causally claim P08; carried search residue can source-causally claim P10. P04 is intentionally narrow: it only claims concrete read/search/git items on real post-compaction rediscovery turns or their immediate carry-forward, not generic self-talk/residual or long-lived residue. P11-P13 are broad fallback owners for otherwise-unclaimed visual probe spillover, tool/search/read/orientation logs, and conversation/reasoning/reconciliation residue, including same-turn residue. Equal turn-level splitting is used only as a legacy fallback when no item ledger is available. Remaining unlabeled outcome residue is counted as waste but not assigned to P01-P13.",
|
|
308
|
+
pressureDefinition: "Qualified pressure is the sum of official JSONL input_tokens on turns with probable or definitive evidence for a problem. It is shown as supporting evidence, not exact item-level waste."
|
|
309
|
+
},
|
|
310
|
+
totals: {
|
|
311
|
+
officialInputTokens: officialTotal,
|
|
312
|
+
analyzedInputTokens: officialTotal,
|
|
313
|
+
classifiedInputTokens: Math.round(classifiedTotal),
|
|
314
|
+
usefulTokens: totals.usefulTokens,
|
|
315
|
+
wasteTokens: totals.wasteTokens,
|
|
316
|
+
usefulSharePct: officialTotal ? round((totals.usefulTokens / officialTotal) * 100, 1) : 0,
|
|
317
|
+
wasteSharePct: officialTotal ? round((totals.wasteTokens / officialTotal) * 100, 1) : 0,
|
|
318
|
+
duplicateWasteTokens: totals.duplicateWasteTokens,
|
|
319
|
+
refindWasteTokens: totals.refindWasteTokens,
|
|
320
|
+
deadWasteTokens: totals.deadWasteTokens,
|
|
321
|
+
wasteCoveredByProblems: Math.round(wasteCoveredByProblems),
|
|
322
|
+
wasteCoveredByProblemsPct: totals.wasteTokens ? round((wasteCoveredByProblems / totals.wasteTokens) * 100, 1) : 0,
|
|
323
|
+
unattributedWasteTokens: Math.round(unlabeledResidueTokens),
|
|
324
|
+
unattributedWastePct: totals.wasteTokens ? round((unlabeledResidueTokens / totals.wasteTokens) * 100, 1) : 0,
|
|
325
|
+
unlabeledResidueTokens: Math.round(unlabeledResidueTokens),
|
|
326
|
+
unlabeledResiduePctOfInput: officialTotal ? round((unlabeledResidueTokens / officialTotal) * 100, 1) : 0,
|
|
327
|
+
overheadUsefulTokens: Math.round(overheadUsefulTokens),
|
|
328
|
+
overheadUsefulPctOfInput: officialTotal ? round((overheadUsefulTokens / officialTotal) * 100, 1) : 0,
|
|
329
|
+
excludedUnlabeledTokens: 0,
|
|
330
|
+
excludedUnlabeledPct: 0,
|
|
331
|
+
excludedOverheadTokens: 0,
|
|
332
|
+
excludedOverheadPct: 0,
|
|
333
|
+
grossQualifiedProblemPressure: tokenPressureTotal,
|
|
334
|
+
uniqueProblemTurnPressure: problemReport.totals?.uniqueProblemTurnTokenPressure || null
|
|
335
|
+
},
|
|
336
|
+
majorContributor: {
|
|
337
|
+
byAllocatedWaste: pickMajor(sortedByAllocatedWaste[0]),
|
|
338
|
+
byQualifiedPressure: pickMajor(sortedByQualifiedPressure[0]),
|
|
339
|
+
interpretation:
|
|
340
|
+
sortedByAllocatedWaste[0]?.id === sortedByQualifiedPressure[0]?.id
|
|
341
|
+
? `${sortedByAllocatedWaste[0].id} is largest by both allocated waste and qualified token pressure.`
|
|
342
|
+
: `${sortedByAllocatedWaste[0]?.id} is largest by allocated waste, while ${sortedByQualifiedPressure[0]?.id} is largest by qualified token pressure.`
|
|
343
|
+
},
|
|
344
|
+
wasteBuckets: [
|
|
345
|
+
{ id: "useful", label: "Useful + required overhead", tokens: totals.usefulTokens, sharePct: officialTotal ? round((totals.usefulTokens / officialTotal) * 100, 1) : 0 },
|
|
346
|
+
{ id: "duplicate", label: "Duplicate waste", tokens: totals.duplicateWasteTokens, sharePct: officialTotal ? round((totals.duplicateWasteTokens / officialTotal) * 100, 1) : 0 },
|
|
347
|
+
{ id: "refind", label: "Re-find waste", tokens: totals.refindWasteTokens, sharePct: officialTotal ? round((totals.refindWasteTokens / officialTotal) * 100, 1) : 0 },
|
|
348
|
+
{ id: "dead", label: "Dead work waste", tokens: totals.deadWasteTokens, sharePct: officialTotal ? round((totals.deadWasteTokens / officialTotal) * 100, 1) : 0 }
|
|
349
|
+
],
|
|
350
|
+
problemLegend: buildProblemLegend(allProblems),
|
|
351
|
+
problemContributions: sortedByAllocatedWaste,
|
|
352
|
+
episodeSummaries,
|
|
353
|
+
keyDataPoints,
|
|
354
|
+
topWasteTurns: [...timeline].sort((a, b) => b.wasteTokens - a.wasteTokens).slice(0, 12),
|
|
355
|
+
topInputTurns: [...timeline].sort((a, b) => b.inputTokens - a.inputTokens).slice(0, 12),
|
|
356
|
+
timeline
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function allocateTurnWaste({ row, turnProblems, wasteTokens, problemByTurn }) {
|
|
361
|
+
const rawItems = Array.isArray(row.wasteItems) ? row.wasteItems.filter((item) => Number(item.tokens || 0) > 0) : [];
|
|
362
|
+
if (!rawItems.length) return allocateTurnWasteFallback({ row, turnProblems, wasteTokens });
|
|
363
|
+
|
|
364
|
+
const problemSet = new Set(turnProblems);
|
|
365
|
+
const problemItems = [];
|
|
366
|
+
const unattributedItems = [];
|
|
367
|
+
let coveredWasteTokens = 0;
|
|
368
|
+
let attributedItemTokens = 0;
|
|
369
|
+
const rawItemTokens = sum(rawItems, (item) => Number(item.tokens || 0));
|
|
370
|
+
const scale = rawItemTokens > wasteTokens && rawItemTokens > 0 ? wasteTokens / rawItemTokens : 1;
|
|
371
|
+
|
|
372
|
+
for (const rawItem of rawItems) {
|
|
373
|
+
const tokens = Number(rawItem.tokens || 0) * scale;
|
|
374
|
+
if (!tokens) continue;
|
|
375
|
+
attributedItemTokens += tokens;
|
|
376
|
+
const item = {
|
|
377
|
+
...rawItem,
|
|
378
|
+
tokens,
|
|
379
|
+
...problemForWasteItem(rawItem, problemSet, problemByTurn, row.turn)
|
|
380
|
+
};
|
|
381
|
+
if (item.problemId) {
|
|
382
|
+
problemItems.push(item);
|
|
383
|
+
coveredWasteTokens += tokens;
|
|
384
|
+
} else {
|
|
385
|
+
unattributedItems.push(item);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const itemTokenGap = Math.max(0, wasteTokens - attributedItemTokens);
|
|
390
|
+
if (itemTokenGap > 0) {
|
|
391
|
+
unattributedItems.push({
|
|
392
|
+
sourceTurn: row.turn,
|
|
393
|
+
kind: "unrepresented",
|
|
394
|
+
bucket: "opt_refind",
|
|
395
|
+
tokens: itemTokenGap,
|
|
396
|
+
assignmentBasis: "baseline",
|
|
397
|
+
reason: "waste total not represented by concrete item ledger"
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const groupedProblemItems = groupAllocatedItems(problemItems);
|
|
402
|
+
const groupedUnattributedItems = groupUnattributedItems(unattributedItems);
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
method: "item_level",
|
|
406
|
+
coveredWasteTokens,
|
|
407
|
+
unattributedWasteTokens: Math.max(0, wasteTokens - coveredWasteTokens),
|
|
408
|
+
problemItems: groupedProblemItems,
|
|
409
|
+
unattributedItems: groupedUnattributedItems,
|
|
410
|
+
problemIds: [...new Set(groupedProblemItems.map((item) => item.problemId))]
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function allocateTurnWasteFallback({ row, turnProblems, wasteTokens }) {
|
|
415
|
+
if (!turnProblems.length) {
|
|
416
|
+
return {
|
|
417
|
+
method: "unattributed",
|
|
418
|
+
coveredWasteTokens: 0,
|
|
419
|
+
unattributedWasteTokens: wasteTokens,
|
|
420
|
+
problemItems: [],
|
|
421
|
+
unattributedItems: wasteTokens > 0 ? [{
|
|
422
|
+
sourceTurn: row.turn,
|
|
423
|
+
kind: "turn_waste",
|
|
424
|
+
bucket: "mixed",
|
|
425
|
+
tokens: wasteTokens,
|
|
426
|
+
assignmentBasis: "baseline",
|
|
427
|
+
reason: "no qualified P-problem label detected on this turn"
|
|
428
|
+
}] : [],
|
|
429
|
+
problemIds: []
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const perProblemWaste = wasteTokens / turnProblems.length;
|
|
434
|
+
return {
|
|
435
|
+
method: "turn_equal_split",
|
|
436
|
+
coveredWasteTokens: wasteTokens,
|
|
437
|
+
unattributedWasteTokens: 0,
|
|
438
|
+
problemItems: turnProblems.map((problemId) => ({
|
|
439
|
+
sourceTurn: row.turn,
|
|
440
|
+
kind: "turn_waste",
|
|
441
|
+
bucket: "mixed",
|
|
442
|
+
tokens: perProblemWaste,
|
|
443
|
+
problemId,
|
|
444
|
+
assignmentBasis: "turn-fallback",
|
|
445
|
+
reason: "legacy fallback: no item-level waste ledger available"
|
|
446
|
+
})),
|
|
447
|
+
unattributedItems: [],
|
|
448
|
+
problemIds: [...turnProblems]
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function problemForWasteItem(item, problemSet, problemByTurn = new Map(), currentTurn = null) {
|
|
453
|
+
const kind = item.kind || "";
|
|
454
|
+
const sourceAssignable = kind === "read" || kind === "search" || kind === "command";
|
|
455
|
+
if (item.imageOutput && sourceAssignable) return { problemId: "P01", assignmentBasis: "image-output" };
|
|
456
|
+
|
|
457
|
+
for (const problemId of DIRECT_PROBLEM_PRIORITY) {
|
|
458
|
+
if (problemMatchesItem(problemId, item, problemSet, currentTurn)) return { problemId, assignmentBasis: "direct-item" };
|
|
459
|
+
}
|
|
460
|
+
const sourceTurn = item.sourceTurn ?? currentTurn;
|
|
461
|
+
const sourceProblemSet = new Set(problemByTurn.get(sourceTurn) || []);
|
|
462
|
+
const command = String(item.command || item.normalizedCommand || "");
|
|
463
|
+
const commandIsGit = /^git(?:\s|$)/.test(command.trim());
|
|
464
|
+
if (kind === "command" && commandIsGit && sourceProblemSet.has("P08")) return { problemId: "P08", assignmentBasis: "source-causal" };
|
|
465
|
+
if (kind === "search" && sourceProblemSet.has("P10")) return { problemId: "P10", assignmentBasis: "source-causal" };
|
|
466
|
+
if (isConcreteP04RediscoveryItem(item) && sourceProblemSet.has("P04") && sourceTurnAge(item, currentTurn) <= 1) {
|
|
467
|
+
return { problemId: "P04", assignmentBasis: "source-causal" };
|
|
468
|
+
}
|
|
469
|
+
const fallbackProblemId = fallbackProblemForBaselineItem(item, currentTurn);
|
|
470
|
+
if (fallbackProblemId) return { problemId: fallbackProblemId, assignmentBasis: "baseline-derived" };
|
|
471
|
+
return { problemId: null, assignmentBasis: "baseline" };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function problemMatchesItem(problemId, item, problemSet, currentTurn = null) {
|
|
475
|
+
const kind = item.kind || "";
|
|
476
|
+
const bucket = item.bucket || "";
|
|
477
|
+
const command = String(item.command || item.normalizedCommand || "");
|
|
478
|
+
const commandIsGit = /^git(?:\s|$)/.test(command.trim());
|
|
479
|
+
const commandIsRun = /^(npm|pnpm|yarn|node|npx|pytest|cargo|go)(?:\s|$)/.test(command.trim());
|
|
480
|
+
const isRediscovery = kind === "read" || kind === "search" || kind === "command" || kind === "residual" || kind === "conv_agent";
|
|
481
|
+
const isRepairLocal = sourceTurnAge(item, currentTurn) <= 1;
|
|
482
|
+
|
|
483
|
+
if (problemId === "P07") return problemSet.has("P07") && isRediscovery;
|
|
484
|
+
if (problemId === "P02") return problemSet.has("P02") && isRediscovery && isRepairLocal;
|
|
485
|
+
if (problemId === "P08") return kind === "command" && commandIsGit && problemSet.has("P08");
|
|
486
|
+
if (problemId === "P10") return kind === "search" && problemSet.has("P10");
|
|
487
|
+
if (problemId === "P06") return problemSet.has("P06") && isRediscovery;
|
|
488
|
+
if (problemId === "P04") return problemSet.has("P04") && isConcreteP04RediscoveryItem(item) && sourceTurnAge(item, currentTurn) <= 1;
|
|
489
|
+
if (problemId === "P05") return problemSet.has("P05") && isRediscovery && isRepairLocal;
|
|
490
|
+
if (problemId === "P03") return (kind === "read" && bucket === "opt_dup") || (kind === "written" && bucket === "opt_dup");
|
|
491
|
+
if (problemId === "P09") return problemSet.has("P09") && (commandIsRun || isRediscovery) && isRepairLocal;
|
|
492
|
+
if (problemId === "P01") return kind === "image" && bucket === "opt_dead";
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function isConcreteP04RediscoveryItem(item) {
|
|
497
|
+
const kind = item.kind || "";
|
|
498
|
+
if (kind === "read" || kind === "search") return true;
|
|
499
|
+
if (kind !== "command") return false;
|
|
500
|
+
const command = String(item.command || item.normalizedCommand || "").trim();
|
|
501
|
+
return /^git(?:\s|$)/.test(command);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function sourceTurnAge(item, currentTurn = null) {
|
|
505
|
+
if (currentTurn == null) return 0;
|
|
506
|
+
const sourceTurn = item.sourceTurn ?? currentTurn;
|
|
507
|
+
return Math.max(0, currentTurn - sourceTurn);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function fallbackProblemForBaselineItem(item, currentTurn = null) {
|
|
511
|
+
const kind = item.kind || "";
|
|
512
|
+
const bucket = item.bucket || "";
|
|
513
|
+
const command = String(item.command || item.normalizedCommand || "");
|
|
514
|
+
if (isVisualProbeCommand(command)) return "P11";
|
|
515
|
+
if (kind === "command" || kind === "search" || kind === "read" && bucket === "opt_dead" || isOrientationCommand(command)) return "P12";
|
|
516
|
+
if (kind === "reasoning" || kind === "residual" || kind === "conv_agent" || kind === "conv_user" || kind === "unrepresented") return "P13";
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function isVisualProbeCommand(command) {
|
|
521
|
+
const normalized = command.trim();
|
|
522
|
+
return normalized === "js"
|
|
523
|
+
|| /^curl\s+-I\s+https?:\/\/(?:127\.0\.0\.1|localhost|\[?::1\]?)(?::|\b|\/)/.test(normalized);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function isOrientationCommand(command) {
|
|
527
|
+
const normalized = command.trim();
|
|
528
|
+
return normalized === "search_memories"
|
|
529
|
+
|| normalized === "search_memories_by_keywords"
|
|
530
|
+
|| /^git(?:\s|$)/.test(normalized)
|
|
531
|
+
|| /^ls(?:\s|$)/.test(normalized);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function groupAllocatedItems(items) {
|
|
535
|
+
const groups = new Map();
|
|
536
|
+
for (const item of items) {
|
|
537
|
+
const key = [
|
|
538
|
+
item.problemId,
|
|
539
|
+
item.sourceTurn || "",
|
|
540
|
+
item.assignmentBasis || "",
|
|
541
|
+
item.bucket,
|
|
542
|
+
item.kind,
|
|
543
|
+
item.file || "",
|
|
544
|
+
item.normalizedCommand || item.command || "",
|
|
545
|
+
item.reason || ""
|
|
546
|
+
].join("\u0000");
|
|
547
|
+
const group = groups.get(key) || {
|
|
548
|
+
problemId: item.problemId,
|
|
549
|
+
sourceTurn: item.sourceTurn,
|
|
550
|
+
assignmentBasis: item.assignmentBasis,
|
|
551
|
+
bucket: item.bucket,
|
|
552
|
+
kind: item.kind,
|
|
553
|
+
file: item.file,
|
|
554
|
+
command: item.normalizedCommand || item.command,
|
|
555
|
+
reason: item.reason,
|
|
556
|
+
tokens: 0,
|
|
557
|
+
count: 0
|
|
558
|
+
};
|
|
559
|
+
group.tokens += item.tokens;
|
|
560
|
+
group.count += 1;
|
|
561
|
+
groups.set(key, group);
|
|
562
|
+
}
|
|
563
|
+
return [...groups.values()];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function compactTimelineItems(items, turn) {
|
|
567
|
+
return items.map((item) => ({
|
|
568
|
+
turn,
|
|
569
|
+
sourceTurn: item.sourceTurn ?? turn,
|
|
570
|
+
assignmentBasis: item.assignmentBasis || "baseline",
|
|
571
|
+
problemId: item.problemId || null,
|
|
572
|
+
bucket: item.bucket || null,
|
|
573
|
+
kind: item.kind || null,
|
|
574
|
+
tokens: Math.round(item.tokens || 0),
|
|
575
|
+
count: item.count || 1,
|
|
576
|
+
file: item.file || null,
|
|
577
|
+
command: item.command || null,
|
|
578
|
+
reason: item.reason || ""
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function groupUnattributedItems(items) {
|
|
583
|
+
const groups = new Map();
|
|
584
|
+
for (const item of items) {
|
|
585
|
+
const key = [item.sourceTurn || "", item.assignmentBasis || "", item.bucket, item.kind, item.file || "", item.reason || ""].join("\u0000");
|
|
586
|
+
const group = groups.get(key) || {
|
|
587
|
+
sourceTurn: item.sourceTurn,
|
|
588
|
+
assignmentBasis: item.assignmentBasis || "baseline",
|
|
589
|
+
bucket: item.bucket,
|
|
590
|
+
kind: item.kind,
|
|
591
|
+
file: item.file,
|
|
592
|
+
reason: item.reason,
|
|
593
|
+
tokens: 0,
|
|
594
|
+
count: 0
|
|
595
|
+
};
|
|
596
|
+
group.tokens += item.tokens;
|
|
597
|
+
group.count += 1;
|
|
598
|
+
groups.set(key, group);
|
|
599
|
+
}
|
|
600
|
+
return [...groups.values()];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function sumEfficiency(rows) {
|
|
604
|
+
const duplicateWasteTokens = sum(rows, (row) => row.opt_dup || 0);
|
|
605
|
+
const refindWasteTokens = sum(rows, (row) => row.opt_refind || 0);
|
|
606
|
+
const deadWasteTokens = sum(rows, (row) => row.opt_dead || 0);
|
|
607
|
+
const usefulTokens = sum(rows, (row) => (row.keep_oh || 0) + (row.keep_prod || 0));
|
|
608
|
+
return {
|
|
609
|
+
officialInputTokens: sum(rows, (row) => row.C_t || 0),
|
|
610
|
+
usefulTokens,
|
|
611
|
+
wasteTokens: duplicateWasteTokens + refindWasteTokens + deadWasteTokens,
|
|
612
|
+
duplicateWasteTokens,
|
|
613
|
+
refindWasteTokens,
|
|
614
|
+
deadWasteTokens
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function sumLabeledBuckets(items) {
|
|
619
|
+
return {
|
|
620
|
+
duplicateTokens: Math.round(sum(items, (item) => item.bucket === "opt_dup" ? item.tokens || 0 : 0)),
|
|
621
|
+
refindTokens: Math.round(sum(items, (item) => item.bucket === "opt_refind" ? item.tokens || 0 : 0)),
|
|
622
|
+
deadTokens: Math.round(sum(items, (item) => item.bucket === "opt_dead" ? item.tokens || 0 : 0))
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function sumDashboardTimeline(timeline) {
|
|
627
|
+
return {
|
|
628
|
+
officialInputTokens: sum(timeline, (turn) => turn.officialInputTokens || 0),
|
|
629
|
+
analyzedInputTokens: sum(timeline, (turn) => turn.inputTokens || 0),
|
|
630
|
+
classifiedInputTokens: sum(timeline, (turn) => turn.classifiedInputTokens || 0),
|
|
631
|
+
usefulTokens: sum(timeline, (turn) => turn.usefulTokens || 0),
|
|
632
|
+
wasteTokens: sum(timeline, (turn) => turn.wasteTokens || 0),
|
|
633
|
+
duplicateWasteTokens: sum(timeline, (turn) => turn.duplicateTokens || 0),
|
|
634
|
+
refindWasteTokens: sum(timeline, (turn) => turn.refindTokens || 0),
|
|
635
|
+
deadWasteTokens: sum(timeline, (turn) => turn.deadTokens || 0)
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function buildTimelineSegments({ exactTotal, usefulTokens, wasteTokens, allocation, problemMetaById }) {
|
|
640
|
+
const segments = [];
|
|
641
|
+
if (usefulTokens > 0) {
|
|
642
|
+
segments.push({
|
|
643
|
+
id: "useful",
|
|
644
|
+
type: "useful",
|
|
645
|
+
label: "Useful content",
|
|
646
|
+
tokens: usefulTokens,
|
|
647
|
+
sharePct: exactTotal ? (usefulTokens / exactTotal) * 100 : 0,
|
|
648
|
+
color: USEFUL_COLOR
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (wasteTokens <= 0) return segments;
|
|
653
|
+
|
|
654
|
+
const problemTotals = new Map();
|
|
655
|
+
for (const item of allocation.problemItems) {
|
|
656
|
+
problemTotals.set(item.problemId, (problemTotals.get(item.problemId) || 0) + item.tokens);
|
|
657
|
+
}
|
|
658
|
+
for (const [problemId, tokens] of problemTotals) {
|
|
659
|
+
const problem = problemMetaById.get(problemId);
|
|
660
|
+
segments.push({
|
|
661
|
+
id: problemId,
|
|
662
|
+
type: "waste",
|
|
663
|
+
label: problem?.name || problemId,
|
|
664
|
+
tokens,
|
|
665
|
+
sharePct: exactTotal ? (tokens / exactTotal) * 100 : 0,
|
|
666
|
+
color: PROBLEM_COLORS[problemId] || "#475467"
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (allocation.unattributedWasteTokens > 0) {
|
|
670
|
+
segments.push({
|
|
671
|
+
id: UNATTRIBUTED_WASTE.id,
|
|
672
|
+
type: "waste",
|
|
673
|
+
label: UNATTRIBUTED_WASTE.name,
|
|
674
|
+
tokens: allocation.unattributedWasteTokens,
|
|
675
|
+
sharePct: exactTotal ? (allocation.unattributedWasteTokens / exactTotal) * 100 : 0,
|
|
676
|
+
color: UNATTRIBUTED_WASTE.color
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return segments;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function buildProblemLegend(problems) {
|
|
683
|
+
return [
|
|
684
|
+
{
|
|
685
|
+
id: "useful",
|
|
686
|
+
name: "Useful + required overhead",
|
|
687
|
+
definition: "Required overhead plus product/task payload content retained as useful.",
|
|
688
|
+
color: USEFUL_COLOR,
|
|
689
|
+
type: "useful"
|
|
690
|
+
},
|
|
691
|
+
...problems.map((problem) => ({
|
|
692
|
+
id: problem.id,
|
|
693
|
+
name: problem.name,
|
|
694
|
+
category: problem.category,
|
|
695
|
+
definition: PROBLEM_DEFINITIONS[problem.id] || "",
|
|
696
|
+
color: PROBLEM_COLORS[problem.id] || "#475467",
|
|
697
|
+
type: "problem"
|
|
698
|
+
})),
|
|
699
|
+
{
|
|
700
|
+
id: UNATTRIBUTED_WASTE.id,
|
|
701
|
+
name: UNATTRIBUTED_WASTE.name,
|
|
702
|
+
definition: "Outcome residue that did not receive a named P-problem label. It is counted as waste, but not attributed to P01-P13.",
|
|
703
|
+
color: UNATTRIBUTED_WASTE.color,
|
|
704
|
+
type: "problem"
|
|
705
|
+
}
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function buildEpisodeSummaries(timeline, episodes) {
|
|
710
|
+
const labelsByNumber = new Map((episodes || []).map((episode, index) => [index + 1, episode.label || `Episode ${index + 1}`]));
|
|
711
|
+
const groups = new Map();
|
|
712
|
+
for (const turn of timeline) {
|
|
713
|
+
const group = groups.get(turn.episode) || {
|
|
714
|
+
episode: turn.episode,
|
|
715
|
+
label: labelsByNumber.get(turn.episode) || `Episode ${turn.episode}`,
|
|
716
|
+
turns: 0,
|
|
717
|
+
inputTokens: 0,
|
|
718
|
+
usefulTokens: 0,
|
|
719
|
+
wasteTokens: 0,
|
|
720
|
+
duplicateTokens: 0,
|
|
721
|
+
refindTokens: 0,
|
|
722
|
+
deadTokens: 0,
|
|
723
|
+
problemTurnCount: 0
|
|
724
|
+
};
|
|
725
|
+
group.turns += 1;
|
|
726
|
+
group.inputTokens += turn.inputTokens;
|
|
727
|
+
group.usefulTokens += turn.usefulTokens;
|
|
728
|
+
group.wasteTokens += turn.wasteTokens;
|
|
729
|
+
group.duplicateTokens += turn.duplicateTokens;
|
|
730
|
+
group.refindTokens += turn.refindTokens;
|
|
731
|
+
group.deadTokens += turn.deadTokens;
|
|
732
|
+
if (turn.problemCount) group.problemTurnCount += 1;
|
|
733
|
+
groups.set(turn.episode, group);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return [...groups.values()].map((episode) => ({
|
|
737
|
+
...episode,
|
|
738
|
+
usefulSharePct: round((episode.usefulTokens / episode.inputTokens) * 100, 1),
|
|
739
|
+
wasteSharePct: round((episode.wasteTokens / episode.inputTokens) * 100, 1)
|
|
740
|
+
}));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function buildKeyDataPoints({ problemReport, timeline, episodeSummaries, totals, sortedByAllocatedWaste }) {
|
|
744
|
+
const peakInputTurn = maxBy(timeline, (turn) => turn.inputTokens);
|
|
745
|
+
const peakWasteTurn = maxBy(timeline, (turn) => turn.wasteTokens);
|
|
746
|
+
const bestUsefulTurn = maxBy(timeline, (turn) => turn.usefulSharePct);
|
|
747
|
+
const worstWasteShareTurn = maxBy(timeline, (turn) => turn.wasteSharePct);
|
|
748
|
+
const worstEpisode = maxBy(episodeSummaries, (episode) => episode.wasteTokens);
|
|
749
|
+
const largestBucket = maxBy(
|
|
750
|
+
[
|
|
751
|
+
{ label: "duplicate", tokens: totals.duplicateWasteTokens },
|
|
752
|
+
{ label: "re-find", tokens: totals.refindWasteTokens },
|
|
753
|
+
{ label: "dead work", tokens: totals.deadWasteTokens }
|
|
754
|
+
],
|
|
755
|
+
(bucket) => bucket.tokens
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
turns: problemReport.session?.turns || timeline.length,
|
|
760
|
+
tokenCountedTurns: problemReport.session?.tokenCountedTurns || timeline.length,
|
|
761
|
+
compactions: problemReport.session?.compactions || 0,
|
|
762
|
+
aborts: problemReport.session?.aborts || 0,
|
|
763
|
+
rollbacks: problemReport.session?.rollbacks || 0,
|
|
764
|
+
turnsWithAnyProblem: problemReport.totals?.turnsWithAnyProblem || 0,
|
|
765
|
+
qualifiedProblemTurnsInTimeline: timeline.filter((turn) => turn.problemCount > 0).length,
|
|
766
|
+
peakInputTurn,
|
|
767
|
+
peakWasteTurn,
|
|
768
|
+
bestUsefulTurn,
|
|
769
|
+
worstWasteShareTurn,
|
|
770
|
+
worstEpisode,
|
|
771
|
+
largestWasteBucket: largestBucket,
|
|
772
|
+
topProblems: sortedByAllocatedWaste.slice(0, 3).map(pickMajor)
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function renderHtml(report) {
|
|
777
|
+
const topContribution = report.problemContributions[0];
|
|
778
|
+
const topPressure = [...report.problemContributions].sort((a, b) => b.qualifiedTokenPressure - a.qualifiedTokenPressure)[0];
|
|
779
|
+
return `<!doctype html>
|
|
780
|
+
<html lang="en">
|
|
781
|
+
<head>
|
|
782
|
+
<meta charset="utf-8">
|
|
783
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
784
|
+
<title>Session Waste Dashboard ${escapeHtml(report.session.id)}</title>
|
|
785
|
+
<style>
|
|
786
|
+
:root {
|
|
787
|
+
--bg: #f7f4ee;
|
|
788
|
+
--ink: #18202d;
|
|
789
|
+
--muted: #667085;
|
|
790
|
+
--line: #ded7cb;
|
|
791
|
+
--panel: #fffdf8;
|
|
792
|
+
--teal: #15837a;
|
|
793
|
+
--green: #3f8f62;
|
|
794
|
+
--amber: #cc8a1e;
|
|
795
|
+
--rose: #c24a4a;
|
|
796
|
+
--blue: #315ea8;
|
|
797
|
+
--violet: #7c5eb8;
|
|
798
|
+
--shadow: 0 18px 48px rgba(45, 36, 20, 0.12);
|
|
799
|
+
}
|
|
800
|
+
* { box-sizing: border-box; }
|
|
801
|
+
body {
|
|
802
|
+
margin: 0;
|
|
803
|
+
background: var(--bg);
|
|
804
|
+
color: var(--ink);
|
|
805
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
806
|
+
line-height: 1.45;
|
|
807
|
+
}
|
|
808
|
+
main { max-width: 1420px; margin: 0 auto; padding: 28px; }
|
|
809
|
+
h1, h2, h3, p { margin: 0; }
|
|
810
|
+
h1 { font-size: clamp(28px, 4vw, 54px); line-height: 1; letter-spacing: 0; }
|
|
811
|
+
h2 { font-size: 18px; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
812
|
+
h3 { font-size: 16px; }
|
|
813
|
+
.hero {
|
|
814
|
+
display: grid;
|
|
815
|
+
grid-template-columns: 1.25fr 0.75fr;
|
|
816
|
+
gap: 22px;
|
|
817
|
+
align-items: stretch;
|
|
818
|
+
margin-bottom: 22px;
|
|
819
|
+
}
|
|
820
|
+
.panel {
|
|
821
|
+
background: var(--panel);
|
|
822
|
+
border: 1px solid var(--line);
|
|
823
|
+
box-shadow: var(--shadow);
|
|
824
|
+
border-radius: 8px;
|
|
825
|
+
}
|
|
826
|
+
.hero-copy { padding: 26px; }
|
|
827
|
+
.eyebrow { color: var(--teal); font-weight: 800; text-transform: uppercase; letter-spacing: 0.12em; font-size: 13px; }
|
|
828
|
+
.subtitle { color: var(--muted); max-width: 820px; margin-top: 14px; font-size: 17px; }
|
|
829
|
+
.method-note { margin-top: 18px; padding: 12px 14px; border-left: 4px solid var(--amber); background: #fff5df; color: #594019; border-radius: 4px; }
|
|
830
|
+
.donut-wrap { display: grid; place-items: center; padding: 26px; }
|
|
831
|
+
.donut {
|
|
832
|
+
width: min(320px, 72vw);
|
|
833
|
+
aspect-ratio: 1;
|
|
834
|
+
border-radius: 50%;
|
|
835
|
+
background:
|
|
836
|
+
radial-gradient(circle at center, var(--panel) 0 55%, transparent 56%),
|
|
837
|
+
conic-gradient(var(--rose) 0 ${report.totals.wasteSharePct}%, var(--green) ${report.totals.wasteSharePct}% 100%);
|
|
838
|
+
display: grid;
|
|
839
|
+
place-items: center;
|
|
840
|
+
border: 1px solid var(--line);
|
|
841
|
+
}
|
|
842
|
+
.donut strong { display: block; font-size: 46px; line-height: 1; }
|
|
843
|
+
.donut span { color: var(--muted); font-weight: 700; text-transform: uppercase; font-size: 12px; letter-spacing: 0.08em; }
|
|
844
|
+
.cards { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 22px; }
|
|
845
|
+
.card { padding: 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; min-height: 112px; }
|
|
846
|
+
.card .label { color: var(--muted); font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
847
|
+
.card .value { font-size: clamp(22px, 3vw, 34px); line-height: 1.05; font-weight: 850; margin-top: 10px; }
|
|
848
|
+
.card .sub { color: var(--muted); margin-top: 8px; font-size: 13px; }
|
|
849
|
+
.grid-2 { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr); gap: 18px; margin-bottom: 22px; }
|
|
850
|
+
.section { padding: 20px; }
|
|
851
|
+
.section-head { display: flex; justify-content: space-between; gap: 16px; align-items: end; margin-bottom: 16px; }
|
|
852
|
+
.small-note { color: var(--muted); font-size: 13px; max-width: 680px; }
|
|
853
|
+
.stack { display: flex; height: 36px; overflow: hidden; border-radius: 6px; border: 1px solid var(--line); background: #eee7dc; }
|
|
854
|
+
.stack span { display: block; min-width: 1px; }
|
|
855
|
+
.legend { display: flex; flex-wrap: wrap; gap: 10px 16px; margin-top: 12px; color: var(--muted); font-size: 13px; }
|
|
856
|
+
.legend i {
|
|
857
|
+
display: inline-block;
|
|
858
|
+
width: 12px;
|
|
859
|
+
height: 12px;
|
|
860
|
+
border-radius: 3px;
|
|
861
|
+
margin-right: 6px;
|
|
862
|
+
vertical-align: -2px;
|
|
863
|
+
box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.18);
|
|
864
|
+
}
|
|
865
|
+
.bar-list { display: grid; gap: 12px; }
|
|
866
|
+
.problem-row {
|
|
867
|
+
display: grid;
|
|
868
|
+
grid-template-columns: 86px minmax(180px, 1fr) 170px;
|
|
869
|
+
gap: 12px;
|
|
870
|
+
align-items: center;
|
|
871
|
+
padding: 12px 0 12px 10px;
|
|
872
|
+
border-top: 1px solid #ebe4d9;
|
|
873
|
+
border-left: 5px solid var(--problem-color, var(--teal));
|
|
874
|
+
}
|
|
875
|
+
.problem-row:first-child { border-top: 0; }
|
|
876
|
+
.problem-id { font-weight: 900; color: var(--teal); }
|
|
877
|
+
.problem-name { font-weight: 800; }
|
|
878
|
+
.problem-meta { color: var(--muted); font-size: 13px; margin-top: 4px; }
|
|
879
|
+
.problem-definition { color: #475467; font-size: 13px; margin-top: 5px; max-width: 780px; }
|
|
880
|
+
.bar-bg { height: 14px; background: #ebe4d9; border-radius: 999px; overflow: hidden; margin-top: 8px; }
|
|
881
|
+
.bar-fill { height: 100%; background: var(--problem-color, var(--teal)); border-radius: inherit; }
|
|
882
|
+
.number { font-variant-numeric: tabular-nums; text-align: right; font-weight: 850; }
|
|
883
|
+
.definition-grid {
|
|
884
|
+
display: grid;
|
|
885
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
886
|
+
gap: 12px;
|
|
887
|
+
}
|
|
888
|
+
.definition-item {
|
|
889
|
+
display: grid;
|
|
890
|
+
grid-template-columns: 42px minmax(0, 1fr);
|
|
891
|
+
gap: 10px;
|
|
892
|
+
padding: 12px;
|
|
893
|
+
border: 1px solid #ebe4d9;
|
|
894
|
+
border-left: 5px solid var(--problem-color, var(--teal));
|
|
895
|
+
border-radius: 8px;
|
|
896
|
+
background: rgba(255, 253, 248, 0.72);
|
|
897
|
+
}
|
|
898
|
+
.definition-item strong {
|
|
899
|
+
display: block;
|
|
900
|
+
font-size: 13px;
|
|
901
|
+
line-height: 1.2;
|
|
902
|
+
}
|
|
903
|
+
.definition-item p {
|
|
904
|
+
margin-top: 4px;
|
|
905
|
+
color: var(--muted);
|
|
906
|
+
font-size: 13px;
|
|
907
|
+
line-height: 1.35;
|
|
908
|
+
}
|
|
909
|
+
.timeline {
|
|
910
|
+
display: grid;
|
|
911
|
+
grid-template-columns: repeat(${Math.max(1, report.timeline.length)}, minmax(7px, 1fr));
|
|
912
|
+
gap: 3px;
|
|
913
|
+
align-items: end;
|
|
914
|
+
min-width: 920px;
|
|
915
|
+
min-height: 178px;
|
|
916
|
+
padding-top: 132px;
|
|
917
|
+
border-bottom: 1px solid var(--line);
|
|
918
|
+
}
|
|
919
|
+
.timeline-frame {
|
|
920
|
+
overflow: visible;
|
|
921
|
+
padding-bottom: 2px;
|
|
922
|
+
}
|
|
923
|
+
.episode-ruler {
|
|
924
|
+
display: grid;
|
|
925
|
+
grid-template-columns: repeat(${Math.max(1, report.timeline.length)}, minmax(7px, 1fr));
|
|
926
|
+
gap: 3px;
|
|
927
|
+
min-width: 920px;
|
|
928
|
+
margin: 2px 0 -58px;
|
|
929
|
+
padding-top: 8px;
|
|
930
|
+
position: relative;
|
|
931
|
+
z-index: 20;
|
|
932
|
+
}
|
|
933
|
+
.episode-band {
|
|
934
|
+
min-width: 0;
|
|
935
|
+
position: relative;
|
|
936
|
+
display: flex;
|
|
937
|
+
align-items: center;
|
|
938
|
+
gap: 6px;
|
|
939
|
+
min-height: 36px;
|
|
940
|
+
border: 1px solid rgba(21, 131, 122, 0.2);
|
|
941
|
+
border-bottom: 3px solid var(--teal);
|
|
942
|
+
border-radius: 8px 8px 4px 4px;
|
|
943
|
+
background: linear-gradient(180deg, rgba(21, 131, 122, 0.13), rgba(255, 253, 248, 0.94));
|
|
944
|
+
padding: 5px 8px;
|
|
945
|
+
color: #144d48;
|
|
946
|
+
overflow: visible;
|
|
947
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
|
|
948
|
+
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
|
|
949
|
+
}
|
|
950
|
+
.episode-band:nth-child(even) {
|
|
951
|
+
border-bottom-color: var(--amber);
|
|
952
|
+
border-color: rgba(204, 138, 30, 0.24);
|
|
953
|
+
background: linear-gradient(180deg, rgba(204, 138, 30, 0.12), rgba(255, 253, 248, 0.9));
|
|
954
|
+
color: #5d4218;
|
|
955
|
+
}
|
|
956
|
+
.episode-band:hover,
|
|
957
|
+
.episode-band:focus {
|
|
958
|
+
z-index: 70;
|
|
959
|
+
transform: translateY(-1px);
|
|
960
|
+
border-color: rgba(17, 24, 39, 0.28);
|
|
961
|
+
box-shadow: 0 10px 24px rgba(24, 32, 45, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.84);
|
|
962
|
+
outline: none;
|
|
963
|
+
}
|
|
964
|
+
.episode-badge {
|
|
965
|
+
display: inline-grid;
|
|
966
|
+
place-items: center;
|
|
967
|
+
flex: 0 0 auto;
|
|
968
|
+
min-width: 25px;
|
|
969
|
+
height: 22px;
|
|
970
|
+
padding: 0 6px;
|
|
971
|
+
border-radius: 999px;
|
|
972
|
+
background: rgba(21, 131, 122, 0.13);
|
|
973
|
+
color: #115a54;
|
|
974
|
+
font-size: 11px;
|
|
975
|
+
font-weight: 900;
|
|
976
|
+
line-height: 1;
|
|
977
|
+
}
|
|
978
|
+
.episode-band:nth-child(even) .episode-badge {
|
|
979
|
+
background: rgba(204, 138, 30, 0.16);
|
|
980
|
+
color: #714b10;
|
|
981
|
+
}
|
|
982
|
+
.episode-label {
|
|
983
|
+
min-width: 0;
|
|
984
|
+
display: grid;
|
|
985
|
+
gap: 1px;
|
|
986
|
+
}
|
|
987
|
+
.episode-label strong {
|
|
988
|
+
display: block;
|
|
989
|
+
font-size: 12px;
|
|
990
|
+
line-height: 1.1;
|
|
991
|
+
white-space: nowrap;
|
|
992
|
+
overflow: hidden;
|
|
993
|
+
text-overflow: ellipsis;
|
|
994
|
+
}
|
|
995
|
+
.episode-label > span {
|
|
996
|
+
display: block;
|
|
997
|
+
margin-top: 3px;
|
|
998
|
+
color: var(--muted);
|
|
999
|
+
font-size: 11px;
|
|
1000
|
+
line-height: 1.1;
|
|
1001
|
+
white-space: nowrap;
|
|
1002
|
+
overflow: hidden;
|
|
1003
|
+
text-overflow: ellipsis;
|
|
1004
|
+
}
|
|
1005
|
+
.episode-band.narrow {
|
|
1006
|
+
justify-content: center;
|
|
1007
|
+
padding: 5px 3px;
|
|
1008
|
+
}
|
|
1009
|
+
.episode-band.narrow .episode-label {
|
|
1010
|
+
display: none;
|
|
1011
|
+
}
|
|
1012
|
+
.episode-tip {
|
|
1013
|
+
position: absolute;
|
|
1014
|
+
left: 50%;
|
|
1015
|
+
top: calc(100% + 8px);
|
|
1016
|
+
z-index: 90;
|
|
1017
|
+
width: 360px;
|
|
1018
|
+
padding: 10px 12px;
|
|
1019
|
+
border-radius: 8px;
|
|
1020
|
+
background: #111827;
|
|
1021
|
+
color: #f9fafb;
|
|
1022
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
1023
|
+
box-shadow: 0 18px 40px rgba(17, 24, 39, 0.26);
|
|
1024
|
+
transform: translateX(-50%) translateY(-4px);
|
|
1025
|
+
opacity: 0;
|
|
1026
|
+
pointer-events: none;
|
|
1027
|
+
transition: opacity 120ms ease, transform 120ms ease;
|
|
1028
|
+
}
|
|
1029
|
+
.episode-band:hover .episode-tip,
|
|
1030
|
+
.episode-band:focus .episode-tip {
|
|
1031
|
+
opacity: 1;
|
|
1032
|
+
transform: translateX(-50%) translateY(0);
|
|
1033
|
+
}
|
|
1034
|
+
.episode-band:first-child .episode-tip {
|
|
1035
|
+
left: 0;
|
|
1036
|
+
transform: translateX(0) translateY(-4px);
|
|
1037
|
+
}
|
|
1038
|
+
.episode-band:first-child:hover .episode-tip,
|
|
1039
|
+
.episode-band:first-child:focus .episode-tip {
|
|
1040
|
+
transform: translateX(0) translateY(0);
|
|
1041
|
+
}
|
|
1042
|
+
.episode-band:last-child .episode-tip {
|
|
1043
|
+
left: auto;
|
|
1044
|
+
right: 0;
|
|
1045
|
+
transform: translateX(0) translateY(-4px);
|
|
1046
|
+
}
|
|
1047
|
+
.episode-band:last-child:hover .episode-tip,
|
|
1048
|
+
.episode-band:last-child:focus .episode-tip {
|
|
1049
|
+
transform: translateX(0) translateY(0);
|
|
1050
|
+
}
|
|
1051
|
+
.episode-tip-title {
|
|
1052
|
+
display: flex;
|
|
1053
|
+
justify-content: space-between;
|
|
1054
|
+
gap: 10px;
|
|
1055
|
+
margin-bottom: 7px;
|
|
1056
|
+
font-weight: 850;
|
|
1057
|
+
}
|
|
1058
|
+
.episode-tip-grid {
|
|
1059
|
+
display: grid;
|
|
1060
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1061
|
+
gap: 6px 12px;
|
|
1062
|
+
font-size: 12px;
|
|
1063
|
+
}
|
|
1064
|
+
.episode-tip-grid span {
|
|
1065
|
+
display: block;
|
|
1066
|
+
}
|
|
1067
|
+
.episode-tip-grid strong {
|
|
1068
|
+
display: block;
|
|
1069
|
+
margin-top: 1px;
|
|
1070
|
+
color: #f9fafb;
|
|
1071
|
+
font-size: 12px;
|
|
1072
|
+
line-height: 1.1;
|
|
1073
|
+
}
|
|
1074
|
+
.episode-tip-muted {
|
|
1075
|
+
color: #cbd5e1;
|
|
1076
|
+
}
|
|
1077
|
+
.turn-bar {
|
|
1078
|
+
min-height: 5px;
|
|
1079
|
+
border-radius: 3px 3px 0 0;
|
|
1080
|
+
outline: 1px solid rgba(24, 32, 45, 0.08);
|
|
1081
|
+
position: relative;
|
|
1082
|
+
display: flex;
|
|
1083
|
+
flex-direction: column-reverse;
|
|
1084
|
+
overflow: visible;
|
|
1085
|
+
background: #ebe4d9;
|
|
1086
|
+
}
|
|
1087
|
+
.turn-bar.has-problem { box-shadow: inset 0 0 0 2px rgba(21, 131, 122, 0.35); }
|
|
1088
|
+
.turn-bar.compaction::after {
|
|
1089
|
+
content: "";
|
|
1090
|
+
position: absolute;
|
|
1091
|
+
top: -8px;
|
|
1092
|
+
left: 50%;
|
|
1093
|
+
width: 5px;
|
|
1094
|
+
height: 5px;
|
|
1095
|
+
border-radius: 50%;
|
|
1096
|
+
background: var(--blue);
|
|
1097
|
+
transform: translateX(-50%);
|
|
1098
|
+
}
|
|
1099
|
+
.turn-segment {
|
|
1100
|
+
width: 100%;
|
|
1101
|
+
min-height: 1px;
|
|
1102
|
+
opacity: 0.98;
|
|
1103
|
+
box-shadow: inset 0 -1px 0 rgba(17, 24, 39, 0.12);
|
|
1104
|
+
}
|
|
1105
|
+
.turn-segment:first-child { border-radius: 0 0 3px 3px; }
|
|
1106
|
+
.turn-segment:last-child { border-radius: 3px 3px 0 0; }
|
|
1107
|
+
.turn-tooltip {
|
|
1108
|
+
position: absolute;
|
|
1109
|
+
left: 50%;
|
|
1110
|
+
bottom: calc(100% + 12px);
|
|
1111
|
+
z-index: 180;
|
|
1112
|
+
width: min(330px, 82vw);
|
|
1113
|
+
padding: 10px 12px;
|
|
1114
|
+
background: #141c2b;
|
|
1115
|
+
color: #f9fafb;
|
|
1116
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
1117
|
+
border-radius: 7px;
|
|
1118
|
+
box-shadow: 0 14px 34px rgba(17, 24, 39, 0.25);
|
|
1119
|
+
transform: translateX(-50%) translateY(6px);
|
|
1120
|
+
opacity: 0;
|
|
1121
|
+
pointer-events: none;
|
|
1122
|
+
transition: opacity 120ms ease, transform 120ms ease;
|
|
1123
|
+
}
|
|
1124
|
+
.turn-bar:hover,
|
|
1125
|
+
.turn-bar:focus {
|
|
1126
|
+
z-index: 160;
|
|
1127
|
+
outline: 2px solid rgba(17, 24, 39, 0.32);
|
|
1128
|
+
filter: saturate(1.08);
|
|
1129
|
+
}
|
|
1130
|
+
.turn-bar:hover .turn-tooltip,
|
|
1131
|
+
.turn-bar:focus .turn-tooltip {
|
|
1132
|
+
opacity: 1;
|
|
1133
|
+
transform: translateX(-50%) translateY(0);
|
|
1134
|
+
}
|
|
1135
|
+
.turn-bar:nth-child(-n + 8) .turn-tooltip {
|
|
1136
|
+
left: 0;
|
|
1137
|
+
transform: translateX(0) translateY(4px);
|
|
1138
|
+
}
|
|
1139
|
+
.turn-bar:nth-child(-n + 8):hover .turn-tooltip,
|
|
1140
|
+
.turn-bar:nth-child(-n + 8):focus .turn-tooltip {
|
|
1141
|
+
transform: translateX(0) translateY(0);
|
|
1142
|
+
}
|
|
1143
|
+
.turn-bar:nth-child(n + ${Math.max(1, report.timeline.length - 7)}) .turn-tooltip {
|
|
1144
|
+
left: auto;
|
|
1145
|
+
right: 0;
|
|
1146
|
+
transform: translateX(0) translateY(4px);
|
|
1147
|
+
}
|
|
1148
|
+
.turn-bar:nth-child(n + ${Math.max(1, report.timeline.length - 7)}):hover .turn-tooltip,
|
|
1149
|
+
.turn-bar:nth-child(n + ${Math.max(1, report.timeline.length - 7)}):focus .turn-tooltip {
|
|
1150
|
+
transform: translateX(0) translateY(0);
|
|
1151
|
+
}
|
|
1152
|
+
.tip-title {
|
|
1153
|
+
display: flex;
|
|
1154
|
+
justify-content: space-between;
|
|
1155
|
+
gap: 12px;
|
|
1156
|
+
font-weight: 850;
|
|
1157
|
+
margin-bottom: 7px;
|
|
1158
|
+
font-size: 12px;
|
|
1159
|
+
}
|
|
1160
|
+
.tip-muted { color: #cbd5e1; font-size: 11px; }
|
|
1161
|
+
.tip-message {
|
|
1162
|
+
margin: 0 0 8px;
|
|
1163
|
+
padding: 7px 8px;
|
|
1164
|
+
border-radius: 5px;
|
|
1165
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1166
|
+
color: #e5e7eb;
|
|
1167
|
+
font-size: 11px;
|
|
1168
|
+
line-height: 1.35;
|
|
1169
|
+
}
|
|
1170
|
+
.tip-message strong {
|
|
1171
|
+
color: #f9fafb;
|
|
1172
|
+
font-weight: 850;
|
|
1173
|
+
}
|
|
1174
|
+
.tip-grid {
|
|
1175
|
+
display: grid;
|
|
1176
|
+
grid-template-columns: 1fr auto;
|
|
1177
|
+
gap: 2px 10px;
|
|
1178
|
+
font-size: 11px;
|
|
1179
|
+
margin-bottom: 7px;
|
|
1180
|
+
}
|
|
1181
|
+
.tip-breakdown {
|
|
1182
|
+
display: grid;
|
|
1183
|
+
gap: 4px;
|
|
1184
|
+
padding-top: 7px;
|
|
1185
|
+
border-top: 1px solid rgba(255, 255, 255, 0.14);
|
|
1186
|
+
font-size: 11px;
|
|
1187
|
+
}
|
|
1188
|
+
.tip-line {
|
|
1189
|
+
display: grid;
|
|
1190
|
+
grid-template-columns: 9px minmax(0, 1fr) auto;
|
|
1191
|
+
gap: 6px;
|
|
1192
|
+
align-items: center;
|
|
1193
|
+
}
|
|
1194
|
+
.tip-swatch {
|
|
1195
|
+
width: 9px;
|
|
1196
|
+
height: 9px;
|
|
1197
|
+
border-radius: 2px;
|
|
1198
|
+
}
|
|
1199
|
+
.timeline-legend {
|
|
1200
|
+
display: flex;
|
|
1201
|
+
flex-wrap: wrap;
|
|
1202
|
+
gap: 8px 12px;
|
|
1203
|
+
margin-top: 14px;
|
|
1204
|
+
}
|
|
1205
|
+
.legend-chip {
|
|
1206
|
+
display: inline-flex;
|
|
1207
|
+
align-items: center;
|
|
1208
|
+
gap: 6px;
|
|
1209
|
+
min-height: 26px;
|
|
1210
|
+
padding: 3px 8px;
|
|
1211
|
+
border: 1px solid var(--line);
|
|
1212
|
+
border-radius: 999px;
|
|
1213
|
+
background: rgba(255, 253, 248, 0.7);
|
|
1214
|
+
color: #334155;
|
|
1215
|
+
font-size: 12px;
|
|
1216
|
+
font-weight: 750;
|
|
1217
|
+
}
|
|
1218
|
+
.legend-chip i {
|
|
1219
|
+
width: 13px;
|
|
1220
|
+
height: 13px;
|
|
1221
|
+
border-radius: 3px;
|
|
1222
|
+
box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.18);
|
|
1223
|
+
}
|
|
1224
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
1225
|
+
th {
|
|
1226
|
+
text-align: left;
|
|
1227
|
+
color: var(--muted);
|
|
1228
|
+
font-size: 12px;
|
|
1229
|
+
letter-spacing: 0.08em;
|
|
1230
|
+
text-transform: uppercase;
|
|
1231
|
+
padding: 10px 8px;
|
|
1232
|
+
border-bottom: 1px solid var(--line);
|
|
1233
|
+
}
|
|
1234
|
+
td { padding: 11px 8px; border-bottom: 1px solid #ebe4d9; vertical-align: top; }
|
|
1235
|
+
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
1236
|
+
.pill {
|
|
1237
|
+
display: inline-flex;
|
|
1238
|
+
align-items: center;
|
|
1239
|
+
min-height: 24px;
|
|
1240
|
+
padding: 2px 8px;
|
|
1241
|
+
border-radius: 999px;
|
|
1242
|
+
background: #e8f3ef;
|
|
1243
|
+
color: #0d6b63;
|
|
1244
|
+
font-size: 12px;
|
|
1245
|
+
font-weight: 850;
|
|
1246
|
+
margin: 0 4px 4px 0;
|
|
1247
|
+
white-space: nowrap;
|
|
1248
|
+
}
|
|
1249
|
+
.callout {
|
|
1250
|
+
border-left: 5px solid var(--teal);
|
|
1251
|
+
background: #ecf7f5;
|
|
1252
|
+
padding: 14px 16px;
|
|
1253
|
+
border-radius: 6px;
|
|
1254
|
+
color: #134b48;
|
|
1255
|
+
margin-top: 16px;
|
|
1256
|
+
}
|
|
1257
|
+
@media (max-width: 1000px) {
|
|
1258
|
+
main { padding: 18px; }
|
|
1259
|
+
.hero, .grid-2 { grid-template-columns: 1fr; }
|
|
1260
|
+
.cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1261
|
+
.problem-row { grid-template-columns: 56px minmax(0, 1fr); }
|
|
1262
|
+
.problem-row .number { grid-column: 1 / -1; text-align: left; }
|
|
1263
|
+
.definition-grid { grid-template-columns: 1fr; }
|
|
1264
|
+
.timeline, .episode-ruler { grid-template-columns: repeat(${Math.max(1, report.timeline.length)}, minmax(6px, 1fr)); min-width: 920px; }
|
|
1265
|
+
.turn-tooltip {
|
|
1266
|
+
position: fixed;
|
|
1267
|
+
left: 18px !important;
|
|
1268
|
+
right: 18px !important;
|
|
1269
|
+
top: 72px;
|
|
1270
|
+
bottom: auto;
|
|
1271
|
+
width: auto;
|
|
1272
|
+
transform: translateY(4px);
|
|
1273
|
+
}
|
|
1274
|
+
.turn-bar:hover .turn-tooltip,
|
|
1275
|
+
.turn-bar:focus .turn-tooltip,
|
|
1276
|
+
.turn-bar:nth-child(-n + 8):hover .turn-tooltip,
|
|
1277
|
+
.turn-bar:nth-child(-n + 8):focus .turn-tooltip,
|
|
1278
|
+
.turn-bar:nth-child(n + ${Math.max(1, report.timeline.length - 7)}):hover .turn-tooltip,
|
|
1279
|
+
.turn-bar:nth-child(n + ${Math.max(1, report.timeline.length - 7)}):focus .turn-tooltip {
|
|
1280
|
+
transform: translateY(0);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
</style>
|
|
1284
|
+
</head>
|
|
1285
|
+
<body>
|
|
1286
|
+
<main>
|
|
1287
|
+
<section class="hero">
|
|
1288
|
+
<div class="panel hero-copy">
|
|
1289
|
+
<p class="eyebrow">Session waste dashboard</p>
|
|
1290
|
+
<h1>10-problem waste contribution</h1>
|
|
1291
|
+
<p class="subtitle">This joins the finalized efficiency buckets with the JSONL-first 10-problem detector for session <strong>${escapeHtml(report.session.id)}</strong>.</p>
|
|
1292
|
+
<div class="method-note">${escapeHtml(report.method.problemAttribution)}</div>
|
|
1293
|
+
</div>
|
|
1294
|
+
<div class="panel donut-wrap">
|
|
1295
|
+
<div class="donut" aria-label="Waste share donut">
|
|
1296
|
+
<div>
|
|
1297
|
+
<strong>${pct(report.totals.wasteSharePct)}</strong>
|
|
1298
|
+
<span>waste</span>
|
|
1299
|
+
</div>
|
|
1300
|
+
</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
</section>
|
|
1303
|
+
|
|
1304
|
+
<section class="cards">
|
|
1305
|
+
${metricCard("Total input", fmt(report.totals.officialInputTokens), "Official JSONL input tokens")}
|
|
1306
|
+
${metricCard("Waste", fmt(report.totals.wasteTokens), `${pct(report.totals.wasteSharePct)} of total input`)}
|
|
1307
|
+
${metricCard("Useful", fmt(report.totals.usefulTokens), `${pct(report.totals.usefulSharePct)} of total input`)}
|
|
1308
|
+
${metricCard("Named P-problem waste", fmt(report.totals.wasteCoveredByProblems), `${pct(report.totals.wasteCoveredByProblemsPct)} of waste`)}
|
|
1309
|
+
${metricCard("Unlabeled residue", fmt(report.totals.unlabeledResidueTokens), `${pct(report.totals.unlabeledResiduePctOfInput)} of input`)}
|
|
1310
|
+
${metricCard("Required overhead", fmt(report.totals.overheadUsefulTokens), `${pct(report.totals.overheadUsefulPctOfInput)} of input`)}
|
|
1311
|
+
${metricCard("Main contributor", `${escapeHtml(topContribution.id)}`, escapeHtml(topContribution.name))}
|
|
1312
|
+
${metricCard("Worst turn", `T${report.keyDataPoints.peakWasteTurn.turn}`, `${fmt(report.keyDataPoints.peakWasteTurn.wasteTokens)} waste tokens`)}
|
|
1313
|
+
</section>
|
|
1314
|
+
|
|
1315
|
+
<section class="grid-2">
|
|
1316
|
+
<div class="panel section">
|
|
1317
|
+
<div class="section-head">
|
|
1318
|
+
<h2>Useful vs Waste</h2>
|
|
1319
|
+
<p class="small-note">Exact totals from the efficiency model.</p>
|
|
1320
|
+
</div>
|
|
1321
|
+
<div class="stack">
|
|
1322
|
+
${report.wasteBuckets.map((bucket) => `<span style="width:${bucket.sharePct}%; background:${bucketColor(bucket.id)}" title="${escapeHtml(bucket.label)} ${fmt(bucket.tokens)}"></span>`).join("")}
|
|
1323
|
+
</div>
|
|
1324
|
+
<div class="legend">
|
|
1325
|
+
${report.wasteBuckets.map((bucket) => `<span><i style="background:${bucketColor(bucket.id)}"></i>${escapeHtml(bucket.label)}: ${fmt(bucket.tokens)} (${pct(bucket.sharePct)})</span>`).join("")}
|
|
1326
|
+
</div>
|
|
1327
|
+
<div class="callout">
|
|
1328
|
+
The biggest waste bucket is <strong>${escapeHtml(report.keyDataPoints.largestWasteBucket.label)}</strong> at ${fmt(report.keyDataPoints.largestWasteBucket.tokens)} tokens.
|
|
1329
|
+
</div>
|
|
1330
|
+
</div>
|
|
1331
|
+
|
|
1332
|
+
<div class="panel section">
|
|
1333
|
+
<div class="section-head">
|
|
1334
|
+
<h2>Major Contributor</h2>
|
|
1335
|
+
<p class="small-note">Two lenses are shown because pressure can overlap across problems.</p>
|
|
1336
|
+
</div>
|
|
1337
|
+
<table>
|
|
1338
|
+
<tbody>
|
|
1339
|
+
<tr><td>Largest by allocated waste</td><td class="num"><strong>${escapeHtml(topContribution.id)} ${escapeHtml(topContribution.name)}</strong><br>${fmt(topContribution.allocatedWasteTokens)} tokens</td></tr>
|
|
1340
|
+
<tr><td>Largest by qualified pressure</td><td class="num"><strong>${escapeHtml(topPressure.id)} ${escapeHtml(topPressure.name)}</strong><br>${fmt(topPressure.qualifiedTokenPressure)} tokens</td></tr>
|
|
1341
|
+
<tr><td>Unlabeled outcome residue</td><td class="num">${fmt(report.totals.unlabeledResidueTokens)} tokens<br>${pct(report.totals.unlabeledResiduePctOfInput)} of input, counted as waste</td></tr>
|
|
1342
|
+
</tbody>
|
|
1343
|
+
</table>
|
|
1344
|
+
<div class="callout">${escapeHtml(report.majorContributor.interpretation)}</div>
|
|
1345
|
+
</div>
|
|
1346
|
+
</section>
|
|
1347
|
+
|
|
1348
|
+
<section class="panel section">
|
|
1349
|
+
<div class="section-head">
|
|
1350
|
+
<h2>10 Problems Contribution</h2>
|
|
1351
|
+
<p class="small-note">Bars show allocated waste share. The right column also shows qualified pressure as a separate evidence measure.</p>
|
|
1352
|
+
</div>
|
|
1353
|
+
<div class="bar-list">
|
|
1354
|
+
${report.problemContributions.map((problem) => problemContributionRow(problem, report.problemContributions[0].allocatedWasteTokens)).join("")}
|
|
1355
|
+
</div>
|
|
1356
|
+
</section>
|
|
1357
|
+
|
|
1358
|
+
<section class="panel section" style="margin-top:22px">
|
|
1359
|
+
<div class="section-head">
|
|
1360
|
+
<h2>Problem Definitions</h2>
|
|
1361
|
+
<p class="small-note">Plain-language meaning of each P label used in the allocation chart and timeline.</p>
|
|
1362
|
+
</div>
|
|
1363
|
+
${problemDefinitionsGrid(report.problemContributions)}
|
|
1364
|
+
</section>
|
|
1365
|
+
|
|
1366
|
+
<section class="panel section" style="margin-top:22px">
|
|
1367
|
+
<div class="section-head">
|
|
1368
|
+
<h2>Whole Session Timeline</h2>
|
|
1369
|
+
<p class="small-note">Each bar is one token-counted turn. Height is official total input; green is useful plus required overhead; colored waste segments include named P-problem waste and unlabeled residue. Hover a bar for details.</p>
|
|
1370
|
+
</div>
|
|
1371
|
+
<div class="timeline-frame">
|
|
1372
|
+
${episodeRuler(report.episodeSummaries)}
|
|
1373
|
+
<div class="timeline">
|
|
1374
|
+
${report.timeline.map((turn) => timelineBar(turn, report.keyDataPoints.peakInputTurn.inputTokens)).join("")}
|
|
1375
|
+
</div>
|
|
1376
|
+
</div>
|
|
1377
|
+
<div class="timeline-legend">
|
|
1378
|
+
${report.problemLegend.map((item) => `<span class="legend-chip" title="${escapeHtml(item.definition || item.name)}"><i style="background:${escapeHtml(item.color)}"></i>${escapeHtml(item.id === "useful" || item.id === "unattributed" ? item.name : `${item.id} ${item.name}`)}</span>`).join("")}
|
|
1379
|
+
<span class="legend-chip"><i style="background:var(--blue); border-radius:50%"></i>Compaction marker</span>
|
|
1380
|
+
</div>
|
|
1381
|
+
</section>
|
|
1382
|
+
|
|
1383
|
+
<section class="grid-2" style="margin-top:22px">
|
|
1384
|
+
<div class="panel section">
|
|
1385
|
+
<div class="section-head">
|
|
1386
|
+
<h2>Episode Summary</h2>
|
|
1387
|
+
</div>
|
|
1388
|
+
${episodeTable(report.episodeSummaries)}
|
|
1389
|
+
</div>
|
|
1390
|
+
<div class="panel section">
|
|
1391
|
+
<div class="section-head">
|
|
1392
|
+
<h2>Key Data Points</h2>
|
|
1393
|
+
</div>
|
|
1394
|
+
<table>
|
|
1395
|
+
<tbody>
|
|
1396
|
+
<tr><td>Turns</td><td class="num">${report.keyDataPoints.turns}</td></tr>
|
|
1397
|
+
<tr><td>Token-counted turns</td><td class="num">${report.keyDataPoints.tokenCountedTurns}</td></tr>
|
|
1398
|
+
<tr><td>Turns with any problem evidence</td><td class="num">${report.keyDataPoints.turnsWithAnyProblem}</td></tr>
|
|
1399
|
+
<tr><td>Qualified problem turns in timeline</td><td class="num">${report.keyDataPoints.qualifiedProblemTurnsInTimeline}</td></tr>
|
|
1400
|
+
<tr><td>Compactions / aborts / rollbacks</td><td class="num">${report.keyDataPoints.compactions} / ${report.keyDataPoints.aborts} / ${report.keyDataPoints.rollbacks}</td></tr>
|
|
1401
|
+
<tr><td>Peak input turn</td><td class="num">T${report.keyDataPoints.peakInputTurn.turn}, ${fmt(report.keyDataPoints.peakInputTurn.inputTokens)}</td></tr>
|
|
1402
|
+
<tr><td>Peak waste turn</td><td class="num">T${report.keyDataPoints.peakWasteTurn.turn}, ${fmt(report.keyDataPoints.peakWasteTurn.wasteTokens)}</td></tr>
|
|
1403
|
+
<tr><td>Worst waste percent turn</td><td class="num">T${report.keyDataPoints.worstWasteShareTurn.turn}, ${pct(report.keyDataPoints.worstWasteShareTurn.wasteSharePct)}</td></tr>
|
|
1404
|
+
</tbody>
|
|
1405
|
+
</table>
|
|
1406
|
+
</div>
|
|
1407
|
+
</section>
|
|
1408
|
+
|
|
1409
|
+
<section class="panel section" style="margin-top:22px">
|
|
1410
|
+
<div class="section-head">
|
|
1411
|
+
<h2>Largest Waste Turns</h2>
|
|
1412
|
+
<p class="small-note">Useful for checking whether the chart matches the underlying session shape.</p>
|
|
1413
|
+
</div>
|
|
1414
|
+
${turnTable(report.topWasteTurns)}
|
|
1415
|
+
</section>
|
|
1416
|
+
</main>
|
|
1417
|
+
</body>
|
|
1418
|
+
</html>`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function problemContributionRow(problem, maxWaste) {
|
|
1422
|
+
const width = maxWaste ? Math.max(2, (problem.allocatedWasteTokens / maxWaste) * 100) : 0;
|
|
1423
|
+
return `<div class="problem-row" style="--problem-color:${escapeHtml(problem.color)}">
|
|
1424
|
+
<div class="problem-id">${escapeHtml(problem.id)}</div>
|
|
1425
|
+
<div>
|
|
1426
|
+
<div class="problem-name">${escapeHtml(problem.name)}</div>
|
|
1427
|
+
<div class="problem-meta">${problem.qualifiedTurns} qualified turns, ${pct(problem.allocatedWasteShareOfTotalPct)} of total waste, ${pct(problem.qualifiedPressureVsSessionPct)} qualified pressure vs session</div>
|
|
1428
|
+
<div class="problem-definition">${escapeHtml(problem.definition)}</div>
|
|
1429
|
+
<div class="bar-bg"><div class="bar-fill" style="width:${round(width, 1)}%"></div></div>
|
|
1430
|
+
</div>
|
|
1431
|
+
<div class="number">${fmt(problem.allocatedWasteTokens)}<br><span class="small-note">${fmt(problem.qualifiedTokenPressure)} pressure</span></div>
|
|
1432
|
+
</div>`;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function problemDefinitionsGrid(problems) {
|
|
1436
|
+
const sorted = [...problems].sort((a, b) => a.id.localeCompare(b.id));
|
|
1437
|
+
return `<div class="definition-grid">
|
|
1438
|
+
${sorted.map((problem) => `<div class="definition-item" style="--problem-color:${escapeHtml(problem.color)}">
|
|
1439
|
+
<div class="problem-id">${escapeHtml(problem.id)}</div>
|
|
1440
|
+
<div>
|
|
1441
|
+
<strong>${escapeHtml(problem.name)}</strong>
|
|
1442
|
+
<p>${escapeHtml(problem.definition)}</p>
|
|
1443
|
+
</div>
|
|
1444
|
+
</div>`).join("")}
|
|
1445
|
+
</div>`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function episodeRuler(episodes) {
|
|
1449
|
+
if (!episodes?.length) return "";
|
|
1450
|
+
return `<div class="episode-ruler" aria-label="Episode ranges">
|
|
1451
|
+
${episodes.map((episode) => {
|
|
1452
|
+
const narrow = episode.turns < 6;
|
|
1453
|
+
const label = `Episode ${episode.episode}: ${episode.label}`;
|
|
1454
|
+
return `<div class="episode-band${narrow ? " narrow" : ""}" style="grid-column: span ${Math.max(1, episode.turns)}" tabindex="0" aria-label="${escapeHtml(label)}">
|
|
1455
|
+
<span class="episode-badge">E${episode.episode}</span>
|
|
1456
|
+
<span class="episode-label">
|
|
1457
|
+
<strong>${escapeHtml(episode.label)}</strong>
|
|
1458
|
+
<span>${fmt(episode.inputTokens)} input · ${pct(episode.wasteSharePct)} waste</span>
|
|
1459
|
+
</span>
|
|
1460
|
+
<span class="episode-tip" role="tooltip">
|
|
1461
|
+
<span class="episode-tip-title"><span>E${episode.episode}</span><span>${escapeHtml(episode.label)}</span></span>
|
|
1462
|
+
<span class="episode-tip-grid">
|
|
1463
|
+
<span><span class="episode-tip-muted">Turns</span><strong>${episode.turns}</strong></span>
|
|
1464
|
+
<span><span class="episode-tip-muted">Input</span><strong>${fmt(episode.inputTokens)}</strong></span>
|
|
1465
|
+
<span><span class="episode-tip-muted">Useful</span><strong>${fmt(episode.usefulTokens)} (${pct(episode.usefulSharePct)})</strong></span>
|
|
1466
|
+
<span><span class="episode-tip-muted">Waste</span><strong>${fmt(episode.wasteTokens)} (${pct(episode.wasteSharePct)})</strong></span>
|
|
1467
|
+
<span style="grid-column:1 / -1"><span class="episode-tip-muted">Duplicate / re-find / dead</span><strong>${fmt(episode.duplicateTokens)} / ${fmt(episode.refindTokens)} / ${fmt(episode.deadTokens)}</strong></span>
|
|
1468
|
+
</span>
|
|
1469
|
+
</span>
|
|
1470
|
+
</div>`;
|
|
1471
|
+
}).join("")}
|
|
1472
|
+
</div>`;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function timelineBar(turn, maxInputTokens) {
|
|
1476
|
+
const height = maxInputTokens ? Math.max(5, (turn.inputTokens / maxInputTokens) * 158) : 5;
|
|
1477
|
+
const classes = ["turn-bar"];
|
|
1478
|
+
if (turn.problemCount) classes.push("has-problem");
|
|
1479
|
+
if (turn.commit || turn.anchorKind === "commit") classes.push("commit");
|
|
1480
|
+
if (turn.compaction) classes.push("compaction");
|
|
1481
|
+
const label = `T${turn.turn}: ${fmt(turn.inputTokens)} input, ${pct(turn.usefulSharePct)} useful, ${pct(turn.wasteSharePct)} waste`;
|
|
1482
|
+
return `<div class="${classes.join(" ")}" style="height:${round(height, 1)}px" tabindex="0" aria-label="${escapeHtml(label)}">
|
|
1483
|
+
${turn.segments.map(timelineSegment).join("")}
|
|
1484
|
+
${timelineTooltip(turn)}
|
|
1485
|
+
</div>`;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function timelineSegment(segment) {
|
|
1489
|
+
return `<span class="turn-segment" style="height:${round(segment.sharePct, 3)}%; background:${escapeHtml(segment.color)}"></span>`;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function timelineTooltip(turn) {
|
|
1493
|
+
const wasteSegments = turn.segments.filter((segment) => segment.type === "waste" && segment.tokens > 0);
|
|
1494
|
+
const userMessage = String(turn.userMessage || "").trim();
|
|
1495
|
+
return `<div class="turn-tooltip" role="tooltip">
|
|
1496
|
+
<div class="tip-title"><span>Turn ${turn.turn}</span><span>${pct(turn.wasteSharePct)} waste</span></div>
|
|
1497
|
+
${userMessage ? `<p class="tip-message"><strong>User:</strong> ${escapeHtml(userMessage)}</p>` : ""}
|
|
1498
|
+
<div class="tip-grid">
|
|
1499
|
+
<span class="tip-muted">Input</span><strong>${fmt(turn.inputTokens)}</strong>
|
|
1500
|
+
<span class="tip-muted">Useful</span><strong>${fmt(turn.usefulTokens)} (${pct(turn.usefulSharePct)})</strong>
|
|
1501
|
+
<span class="tip-muted">Waste</span><strong>${fmt(turn.wasteTokens)} (${pct(turn.wasteSharePct)})</strong>
|
|
1502
|
+
<span class="tip-muted">Product / overhead useful</span><strong>${fmt(turn.productUsefulTokens)} / ${fmt(turn.overheadUsefulTokens)}</strong>
|
|
1503
|
+
<span class="tip-muted">Named / unlabeled waste</span><strong>${fmt(turn.namedWasteTokens)} / ${fmt(turn.unlabeledResidueTokens)}</strong>
|
|
1504
|
+
<span class="tip-muted">Duplicate / re-find / dead</span><strong>${fmt(turn.duplicateTokens)} / ${fmt(turn.refindTokens)} / ${fmt(turn.deadTokens)}</strong>
|
|
1505
|
+
<span class="tip-muted">Flags</span><strong>${turnFlags(turn)}</strong>
|
|
1506
|
+
</div>
|
|
1507
|
+
<div class="tip-breakdown">
|
|
1508
|
+
${wasteSegments.length ? wasteSegments.map((segment) => `<div class="tip-line"><i class="tip-swatch" style="background:${escapeHtml(segment.color)}"></i><span>${escapeHtml(segment.id === "unattributed" ? segment.label : `${segment.id} ${segment.label}`)}</span><strong>${fmt(segment.tokens)} (${pct(segment.sharePct)})</strong></div>`).join("") : "<span class=\"tip-muted\">No waste segment</span>"}
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>`;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function turnFlags(turn) {
|
|
1514
|
+
const flags = [];
|
|
1515
|
+
if (turn.compaction) flags.push("compaction");
|
|
1516
|
+
if (turn.abort) flags.push("abort");
|
|
1517
|
+
if (turn.rollback) flags.push("rollback");
|
|
1518
|
+
if (turn.commit || turn.anchorKind === "commit") flags.push("commit");
|
|
1519
|
+
if (!flags.length) return "none";
|
|
1520
|
+
return escapeHtml(flags.join(", "));
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function episodeTable(episodes) {
|
|
1524
|
+
return `<table>
|
|
1525
|
+
<thead><tr><th>Episode</th><th class="num">Input</th><th class="num">Waste</th><th class="num">Waste %</th></tr></thead>
|
|
1526
|
+
<tbody>
|
|
1527
|
+
${episodes.map((episode) => `<tr><td>${escapeHtml(episode.label)}</td><td class="num">${fmt(episode.inputTokens)}</td><td class="num">${fmt(episode.wasteTokens)}</td><td class="num">${pct(episode.wasteSharePct)}</td></tr>`).join("")}
|
|
1528
|
+
</tbody>
|
|
1529
|
+
</table>`;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function turnTable(turns) {
|
|
1533
|
+
return `<table>
|
|
1534
|
+
<thead><tr><th>Turn</th><th class="num">Input</th><th class="num">Useful</th><th class="num">Waste</th><th>Problems</th></tr></thead>
|
|
1535
|
+
<tbody>
|
|
1536
|
+
${turns.map((turn) => `<tr>
|
|
1537
|
+
<td><strong>T${turn.turn}</strong></td>
|
|
1538
|
+
<td class="num">${fmt(turn.inputTokens)}</td>
|
|
1539
|
+
<td class="num">${fmt(turn.usefulTokens)} (${pct(turn.usefulSharePct)})</td>
|
|
1540
|
+
<td class="num">${fmt(turn.wasteTokens)} (${pct(turn.wasteSharePct)})</td>
|
|
1541
|
+
<td>${turn.problems.length ? turn.problems.map((id) => `<span class="pill">${escapeHtml(id)}</span>`).join("") : "<span class=\"small-note\">none</span>"}</td>
|
|
1542
|
+
</tr>`).join("")}
|
|
1543
|
+
</tbody>
|
|
1544
|
+
</table>`;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function metricCard(label, value, sub) {
|
|
1548
|
+
return `<div class="card"><div class="label">${escapeHtml(label)}</div><div class="value">${value}</div><div class="sub">${escapeHtml(sub)}</div></div>`;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function bucketColor(id) {
|
|
1552
|
+
return {
|
|
1553
|
+
useful: "var(--green)",
|
|
1554
|
+
duplicate: "var(--amber)",
|
|
1555
|
+
refind: "var(--rose)",
|
|
1556
|
+
dead: "var(--violet)"
|
|
1557
|
+
}[id] || "var(--muted)";
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function pickMajor(problem) {
|
|
1561
|
+
if (!problem) return null;
|
|
1562
|
+
return {
|
|
1563
|
+
id: problem.id,
|
|
1564
|
+
name: problem.name,
|
|
1565
|
+
allocatedWasteTokens: problem.allocatedWasteTokens,
|
|
1566
|
+
allocatedWasteShareOfTotalPct: problem.allocatedWasteShareOfTotalPct,
|
|
1567
|
+
qualifiedTokenPressure: problem.qualifiedTokenPressure,
|
|
1568
|
+
qualifiedPressureVsSessionPct: problem.qualifiedPressureVsSessionPct
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function readJson(filePath) {
|
|
1573
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function parseArgs(argv) {
|
|
1577
|
+
const args = { _: [] };
|
|
1578
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1579
|
+
const arg = argv[i];
|
|
1580
|
+
if (!arg.startsWith("--")) {
|
|
1581
|
+
args._.push(arg);
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
const key = arg.slice(2);
|
|
1585
|
+
const next = argv[i + 1];
|
|
1586
|
+
if (!next || next.startsWith("--")) {
|
|
1587
|
+
args[key] = true;
|
|
1588
|
+
} else {
|
|
1589
|
+
args[key] = next;
|
|
1590
|
+
i += 1;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return args;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function sum(items, getter) {
|
|
1597
|
+
return items.reduce((total, item) => total + getter(item), 0);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function maxBy(items, getter) {
|
|
1601
|
+
return items.reduce((best, item) => (!best || getter(item) > getter(best) ? item : best), null);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function mapValues(object, mapper) {
|
|
1605
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, mapper(value)]));
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function fmt(value) {
|
|
1609
|
+
return Math.round(value || 0).toLocaleString("en-US");
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function pct(value) {
|
|
1613
|
+
return `${round(value || 0, 1).toFixed(1)}%`;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function round(value, decimals = 0) {
|
|
1617
|
+
const factor = 10 ** decimals;
|
|
1618
|
+
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function escapeHtml(value) {
|
|
1622
|
+
return String(value ?? "")
|
|
1623
|
+
.replaceAll("&", "&")
|
|
1624
|
+
.replaceAll("<", "<")
|
|
1625
|
+
.replaceAll(">", ">")
|
|
1626
|
+
.replaceAll('"', """)
|
|
1627
|
+
.replaceAll("'", "'");
|
|
1628
|
+
}
|