@oh-my-pi/omp-stats 18.16.0 → 18.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +102 -99
- package/dist/types/client/api.d.ts +4 -1
- package/dist/types/client/app/nav.d.ts +1 -1
- package/dist/types/client/charts/Chart.d.ts +16 -8
- package/dist/types/client/charts/DotWhisker.d.ts +50 -0
- package/dist/types/client/charts/Sparkline.d.ts +1 -1
- package/dist/types/client/charts/Waterfall.d.ts +26 -0
- package/dist/types/client/charts/index.d.ts +2 -0
- package/dist/types/client/charts/types.d.ts +8 -2
- package/dist/types/client/routes/SpotlightRoute.d.ts +7 -0
- package/dist/types/client/routes/index.d.ts +1 -0
- package/dist/types/client/spotlight/Compounding.d.ts +8 -0
- package/dist/types/client/spotlight/Coverage.d.ts +6 -0
- package/dist/types/client/spotlight/Economics.d.ts +10 -0
- package/dist/types/client/spotlight/FetchBack.d.ts +9 -0
- package/dist/types/client/spotlight/Hero.d.ts +12 -0
- package/dist/types/client/spotlight/Overhead.d.ts +6 -0
- package/dist/types/client/spotlight/Ranking.d.ts +6 -0
- package/dist/types/client/spotlight/ResultsFeed.d.ts +6 -0
- package/dist/types/client/spotlight/Savings.d.ts +10 -0
- package/dist/types/client/spotlight/ShareTrend.d.ts +8 -0
- package/dist/types/client/spotlight/Steps.d.ts +6 -0
- package/dist/types/client/spotlight/TimeCost.d.ts +6 -0
- package/dist/types/client/spotlight/chips.d.ts +16 -0
- package/dist/types/client/spotlight/meta.d.ts +64 -0
- package/dist/types/db.d.ts +26 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/parser.d.ts +4 -0
- package/dist/types/rollup.d.ts +22 -1
- package/dist/types/shared-types.d.ts +419 -0
- package/dist/types/spotlight-aggregator.d.ts +189 -0
- package/dist/types/spotlight-bootstrap.d.ts +32 -0
- package/dist/types/spotlight-ingest.d.ts +258 -0
- package/dist/types/spotlight-measures.d.ts +51 -0
- package/package.json +5 -5
- package/src/aggregator.ts +3 -1
- package/src/client/App.tsx +3 -0
- package/src/client/api.ts +21 -0
- package/src/client/app/nav.ts +4 -1
- package/src/client/charts/Chart.tsx +105 -22
- package/src/client/charts/DotWhisker.tsx +149 -0
- package/src/client/charts/Sparkline.tsx +3 -2
- package/src/client/charts/Waterfall.tsx +86 -0
- package/src/client/charts/index.ts +2 -0
- package/src/client/charts/types.ts +9 -2
- package/src/client/react-css.d.ts +1 -1
- package/src/client/routes/SpotlightRoute.tsx +138 -0
- package/src/client/routes/index.ts +1 -0
- package/src/client/spotlight/Compounding.tsx +222 -0
- package/src/client/spotlight/Coverage.tsx +96 -0
- package/src/client/spotlight/Economics.tsx +167 -0
- package/src/client/spotlight/FetchBack.tsx +222 -0
- package/src/client/spotlight/Hero.tsx +177 -0
- package/src/client/spotlight/Overhead.tsx +135 -0
- package/src/client/spotlight/Ranking.tsx +248 -0
- package/src/client/spotlight/ResultsFeed.tsx +354 -0
- package/src/client/spotlight/Savings.tsx +355 -0
- package/src/client/spotlight/ShareTrend.tsx +103 -0
- package/src/client/spotlight/Steps.tsx +225 -0
- package/src/client/spotlight/TimeCost.tsx +64 -0
- package/src/client/spotlight/chips.tsx +63 -0
- package/src/client/spotlight/meta.ts +206 -0
- package/src/client/spotlight/spotlight.css +1387 -0
- package/src/client/styles.css +289 -1
- package/src/db.ts +480 -2
- package/src/index.ts +10 -1
- package/src/live.ts +21 -9
- package/src/parser.ts +44 -8
- package/src/rollup.ts +203 -10
- package/src/server.ts +42 -0
- package/src/shared-types.ts +453 -0
- package/src/spotlight-aggregator.ts +1738 -0
- package/src/spotlight-bootstrap.ts +110 -0
- package/src/spotlight-ingest.ts +981 -0
- package/src/spotlight-measures.ts +667 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Spotlight dashboard's round-2 measures over the analyzed sessions of `spotlight-aggregator.ts`: the
|
|
3
|
+
* holdout-adjusted net, ranking quality against the lines the agent needed later, step × size economics, time cost
|
|
4
|
+
* and tool-output coverage. Metric definitions live on the types in `shared-types.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
import type {
|
|
9
|
+
SpotlightAdjusted,
|
|
10
|
+
SpotlightAdjustedTool,
|
|
11
|
+
SpotlightCompounding,
|
|
12
|
+
SpotlightCoverageRow,
|
|
13
|
+
SpotlightEstimate,
|
|
14
|
+
SpotlightHeatPosition,
|
|
15
|
+
SpotlightRanking,
|
|
16
|
+
SpotlightRankingCell,
|
|
17
|
+
SpotlightSizeBin,
|
|
18
|
+
SpotlightStepCell,
|
|
19
|
+
SpotlightTimeCost,
|
|
20
|
+
SpotlightTool,
|
|
21
|
+
} from "./shared-types";
|
|
22
|
+
import type { Call, Evaluated, Result, SessionAnalysis } from "./spotlight-aggregator";
|
|
23
|
+
import {
|
|
24
|
+
BOOTSTRAP_MIN_SESSIONS,
|
|
25
|
+
bootstrapRatio,
|
|
26
|
+
bootstrapRounds,
|
|
27
|
+
jointSamples,
|
|
28
|
+
percentileInterval,
|
|
29
|
+
} from "./spotlight-bootstrap";
|
|
30
|
+
import { mergeRanges } from "./spotlight-ingest";
|
|
31
|
+
|
|
32
|
+
const CHARS_PER_TOKEN = 4;
|
|
33
|
+
/** Holdouts per tool below which its baseline is too thin to trust. */
|
|
34
|
+
const MIN_HOLDOUTS = 30;
|
|
35
|
+
/** Step combinations with fewer cuts in the range fold into `other`. */
|
|
36
|
+
const MIN_STEP_CUTS = 10;
|
|
37
|
+
/** Original lines a trace's heat covers at most (pi's `SPOTLIGHT_VIEW_LINES`); later lines are unscored. */
|
|
38
|
+
const HEAT_LINES = 5_000;
|
|
39
|
+
/** The hottest share of a result's lines the top lift compares. */
|
|
40
|
+
const TOP_SHARE = 0.1;
|
|
41
|
+
/** Shortest shaped result the position profile reads. */
|
|
42
|
+
const POSITION_MIN_LINES = 40;
|
|
43
|
+
const SIZE_BINS: readonly { bin: SpotlightSizeBin; below: number }[] = [
|
|
44
|
+
{ bin: "<250", below: 250 },
|
|
45
|
+
{ bin: "250–1k", below: 1000 },
|
|
46
|
+
{ bin: "1k–4k", below: 4000 },
|
|
47
|
+
{ bin: "≥4k", below: Number.POSITIVE_INFINITY },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const NO_ESTIMATE: SpotlightEstimate = { value: null, lo: null, hi: null };
|
|
51
|
+
|
|
52
|
+
/** Recovered tokens of a cut: every recovery attributed to it. */
|
|
53
|
+
function recoveredTokens(call: Call): number {
|
|
54
|
+
let tokens = 0;
|
|
55
|
+
for (const recovered of call.recoveredBy) tokens += recovered.tokens;
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Holdout-adjusted net
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/** One experiment session's share of the range's savings and costs, for the adjusted net's joint bootstrap. */
|
|
64
|
+
export interface SessionMoney {
|
|
65
|
+
sessionFile: string;
|
|
66
|
+
savedTokens: number;
|
|
67
|
+
savedUsd: number;
|
|
68
|
+
/** Recovery tokens carried and priced plus extra-turn cost (its share of compounding.recoveryUsd). */
|
|
69
|
+
recoveryUsd: number;
|
|
70
|
+
spotlightUsd: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One tool's cuts and holdouts. */
|
|
74
|
+
interface ToolCell {
|
|
75
|
+
cuts: number;
|
|
76
|
+
recovered: number;
|
|
77
|
+
holdouts: number;
|
|
78
|
+
/** Would-be recovered tokens of the held-out results. */
|
|
79
|
+
baseline: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Cells by tool. */
|
|
83
|
+
type ToolCells = Map<SpotlightTool, ToolCell>;
|
|
84
|
+
|
|
85
|
+
interface AdjustedSession {
|
|
86
|
+
money: SessionMoney;
|
|
87
|
+
tools: ToolCells;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Intervals of the adjusted net's statistics; `excess` by tool, in the order the tools were given. */
|
|
91
|
+
interface AdjustedIntervals {
|
|
92
|
+
total: Pick<SpotlightEstimate, "lo" | "hi">;
|
|
93
|
+
net: Pick<SpotlightEstimate, "lo" | "hi">;
|
|
94
|
+
netUsd: Pick<SpotlightEstimate, "lo" | "hi">;
|
|
95
|
+
excess: Pick<SpotlightEstimate, "lo" | "hi">[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function emptyToolCell(): ToolCell {
|
|
99
|
+
return { cuts: 0, recovered: 0, holdouts: 0, baseline: 0 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The cell of `tool` in `cells`, created empty when missing. */
|
|
103
|
+
function toolCell(cells: ToolCells, tool: SpotlightTool): ToolCell {
|
|
104
|
+
let cell = cells.get(tool);
|
|
105
|
+
if (!cell) {
|
|
106
|
+
cell = emptyToolCell();
|
|
107
|
+
cells.set(tool, cell);
|
|
108
|
+
}
|
|
109
|
+
return cell;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Would-be recovered tokens per held-out cut of a cell: its holdouts' rate, 0 without any. */
|
|
113
|
+
function baselinePerCut(cell: ToolCell): number {
|
|
114
|
+
return cell.holdouts > 0 ? cell.baseline / cell.holdouts : 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Per-cut holdout baselines of the range's evaluated results (see {@link baselinePerCut}): what a cut of `tool`
|
|
119
|
+
* gets fetched back anyway.
|
|
120
|
+
*/
|
|
121
|
+
export function holdoutBaselines(evaluated: readonly Evaluated[]): (tool: SpotlightTool) => number {
|
|
122
|
+
const cells: ToolCells = new Map();
|
|
123
|
+
for (const { call, result, tool } of evaluated) {
|
|
124
|
+
if (!result.holdout) continue;
|
|
125
|
+
const cell = toolCell(cells, tool);
|
|
126
|
+
cell.holdouts++;
|
|
127
|
+
cell.baseline += call.baselineTokens;
|
|
128
|
+
}
|
|
129
|
+
return tool => {
|
|
130
|
+
const cell = cells.get(tool);
|
|
131
|
+
return cell ? baselinePerCut(cell) : 0;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function emptyAdjusted(): SpotlightAdjusted {
|
|
136
|
+
return {
|
|
137
|
+
byTool: [],
|
|
138
|
+
excessTokens: NO_ESTIMATE,
|
|
139
|
+
netTokens: NO_ESTIMATE,
|
|
140
|
+
netUsd: NO_ESTIMATE,
|
|
141
|
+
minHoldouts: MIN_HOLDOUTS,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Excess per tool of summed cells: max(0, recovered − baseline per cut × cuts). */
|
|
146
|
+
function excessOf(cells: ToolCells): Map<SpotlightTool, number> {
|
|
147
|
+
const excess = new Map<SpotlightTool, number>();
|
|
148
|
+
for (const [tool, cell] of cells) excess.set(tool, Math.max(0, cell.recovered - baselinePerCut(cell) * cell.cuts));
|
|
149
|
+
return excess;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Add `cells` into `into`, tool by tool. */
|
|
153
|
+
function addCells(into: ToolCells, cells: ToolCells): void {
|
|
154
|
+
for (const [tool, cell] of cells) {
|
|
155
|
+
const sum = toolCell(into, tool);
|
|
156
|
+
sum.cuts += cell.cuts;
|
|
157
|
+
sum.recovered += cell.recovered;
|
|
158
|
+
sum.holdouts += cell.holdouts;
|
|
159
|
+
sum.baseline += cell.baseline;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function sumOf(values: Iterable<number>): number {
|
|
164
|
+
let sum = 0;
|
|
165
|
+
for (const value of values) sum += value;
|
|
166
|
+
return sum;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function recoveredOf(cells: ToolCells): number {
|
|
170
|
+
let recovered = 0;
|
|
171
|
+
for (const cell of cells.values()) recovered += cell.recovered;
|
|
172
|
+
return recovered;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Holdout-adjusted net savings: each tool's applied cuts (scored and score-free) recover tokens; held-out would-be
|
|
177
|
+
* cuts of the same tool show how many the agent fetches back anyway. Intervals resample experiment sessions
|
|
178
|
+
* jointly. `byTool` lists `tools` in their order.
|
|
179
|
+
*/
|
|
180
|
+
export function computeAdjusted(
|
|
181
|
+
evaluated: readonly Evaluated[],
|
|
182
|
+
money: readonly SessionMoney[],
|
|
183
|
+
grossSavedTokens: number,
|
|
184
|
+
compounding: SpotlightCompounding,
|
|
185
|
+
tools: readonly SpotlightTool[],
|
|
186
|
+
): SpotlightAdjusted {
|
|
187
|
+
const sessions = new Map<string, AdjustedSession>();
|
|
188
|
+
for (const entry of money) sessions.set(entry.sessionFile, { money: entry, tools: new Map() });
|
|
189
|
+
for (const { call, result, analysis, tool } of evaluated) {
|
|
190
|
+
const session = sessions.get(analysis.session.session_file);
|
|
191
|
+
if (!session || (!result.shortened && !result.holdout)) continue;
|
|
192
|
+
const cell = toolCell(session.tools, tool);
|
|
193
|
+
if (result.shortened) {
|
|
194
|
+
cell.cuts++;
|
|
195
|
+
cell.recovered += recoveredTokens(call);
|
|
196
|
+
} else if (result.holdout) {
|
|
197
|
+
cell.holdouts++;
|
|
198
|
+
cell.baseline += call.baselineTokens;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const list = [...sessions.values()];
|
|
202
|
+
const totals: ToolCells = new Map();
|
|
203
|
+
for (const session of list) addCells(totals, session.tools);
|
|
204
|
+
const excess = excessOf(totals);
|
|
205
|
+
const excessTotal = sumOf(excess.values());
|
|
206
|
+
const recoveredTotal = recoveredOf(totals);
|
|
207
|
+
const intervals = list.length >= BOOTSTRAP_MIN_SESSIONS ? adjustedIntervals(list, tools) : null;
|
|
208
|
+
const estimate = (value: number, interval: Pick<SpotlightEstimate, "lo" | "hi"> | undefined): SpotlightEstimate =>
|
|
209
|
+
interval ? { value, ...interval } : { value, lo: null, hi: null };
|
|
210
|
+
|
|
211
|
+
const byTool: SpotlightAdjustedTool[] = tools.map((tool, k) => {
|
|
212
|
+
const cell = totals.get(tool) ?? emptyToolCell();
|
|
213
|
+
const pairs: [number, number][] = [];
|
|
214
|
+
for (const session of list) {
|
|
215
|
+
const own = session.tools.get(tool);
|
|
216
|
+
if (own && own.holdouts > 0) pairs.push([own.baseline, own.holdouts]);
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
tool,
|
|
220
|
+
cuts: cell.cuts,
|
|
221
|
+
recoveredTokens: cell.recovered,
|
|
222
|
+
holdouts: cell.holdouts,
|
|
223
|
+
baselinePerCut: bootstrapRatio(pairs, `adjusted:baseline:${tool}`),
|
|
224
|
+
excessTokens: estimate(excess.get(tool) ?? 0, intervals?.excess[k]),
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
const ratio = recoveredTotal > 0 ? excessTotal / recoveredTotal : 0;
|
|
228
|
+
return {
|
|
229
|
+
byTool,
|
|
230
|
+
excessTokens: estimate(excessTotal, intervals?.total),
|
|
231
|
+
netTokens: estimate(grossSavedTokens - excessTotal, intervals?.net),
|
|
232
|
+
netUsd: estimate(
|
|
233
|
+
compounding.savedUsd - compounding.recoveryUsd * ratio - compounding.spotlightUsd,
|
|
234
|
+
intervals?.netUsd,
|
|
235
|
+
),
|
|
236
|
+
minHoldouts: MIN_HOLDOUTS,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Numbers per cell in {@link adjustedIntervals}' flat cell runs: tool index, cuts, recovered, holdouts, baseline. */
|
|
241
|
+
const RUN_FIELDS = 5;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Intervals of the adjusted net's statistics from one joint bootstrap over the sessions: each round resamples them
|
|
245
|
+
* and applies {@link excessOf}'s rule to the summed cells. Tools are indices and each session's cells one flat run,
|
|
246
|
+
* so a round adds numbers instead of merging maps.
|
|
247
|
+
*/
|
|
248
|
+
function adjustedIntervals(list: readonly AdjustedSession[], tools: readonly SpotlightTool[]): AdjustedIntervals {
|
|
249
|
+
const toolIndex = new Map<SpotlightTool, number>();
|
|
250
|
+
const runs: number[] = [];
|
|
251
|
+
const starts = new Uint32Array(list.length + 1);
|
|
252
|
+
list.forEach((session, i) => {
|
|
253
|
+
starts[i] = runs.length;
|
|
254
|
+
for (const [tool, cell] of session.tools) {
|
|
255
|
+
let index = toolIndex.get(tool);
|
|
256
|
+
if (index === undefined) {
|
|
257
|
+
index = toolIndex.size;
|
|
258
|
+
toolIndex.set(tool, index);
|
|
259
|
+
}
|
|
260
|
+
runs.push(index, cell.cuts, cell.recovered, cell.holdouts, cell.baseline);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
starts[list.length] = runs.length;
|
|
264
|
+
const toolSlot = tools.map(tool => toolIndex.get(tool) ?? -1);
|
|
265
|
+
|
|
266
|
+
// cuts, recovered, holdouts, baseline per tool.
|
|
267
|
+
const cells = new Float64Array(toolIndex.size * 4);
|
|
268
|
+
const toolExcess = new Float64Array(toolIndex.size);
|
|
269
|
+
const total = jointSamples();
|
|
270
|
+
const net = jointSamples();
|
|
271
|
+
const netUsd = jointSamples();
|
|
272
|
+
const excess = tools.map(() => jointSamples());
|
|
273
|
+
bootstrapRounds("adjusted", (random, r) => {
|
|
274
|
+
cells.fill(0);
|
|
275
|
+
let saved = 0;
|
|
276
|
+
let savedUsd = 0;
|
|
277
|
+
let recoveryUsd = 0;
|
|
278
|
+
let spotlightUsd = 0;
|
|
279
|
+
for (let i = 0; i < list.length; i++) {
|
|
280
|
+
const pick = Math.floor(random() * list.length);
|
|
281
|
+
const money = list[pick].money;
|
|
282
|
+
saved += money.savedTokens;
|
|
283
|
+
savedUsd += money.savedUsd;
|
|
284
|
+
recoveryUsd += money.recoveryUsd;
|
|
285
|
+
spotlightUsd += money.spotlightUsd;
|
|
286
|
+
for (let at = starts[pick]; at < starts[pick + 1]; at += RUN_FIELDS) {
|
|
287
|
+
const cell = runs[at] * 4;
|
|
288
|
+
cells[cell] += runs[at + 1];
|
|
289
|
+
cells[cell + 1] += runs[at + 2];
|
|
290
|
+
cells[cell + 2] += runs[at + 3];
|
|
291
|
+
cells[cell + 3] += runs[at + 4];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let sum = 0;
|
|
295
|
+
let recovered = 0;
|
|
296
|
+
for (let t = 0; t < toolIndex.size; t++) {
|
|
297
|
+
const holdouts = cells[t * 4 + 2];
|
|
298
|
+
const perCut = holdouts > 0 ? cells[t * 4 + 3] / holdouts : 0;
|
|
299
|
+
const toolSum = Math.max(0, cells[t * 4 + 1] - perCut * cells[t * 4]);
|
|
300
|
+
toolExcess[t] = toolSum;
|
|
301
|
+
sum += toolSum;
|
|
302
|
+
recovered += cells[t * 4 + 1];
|
|
303
|
+
}
|
|
304
|
+
total[r] = sum;
|
|
305
|
+
net[r] = saved - sum;
|
|
306
|
+
netUsd[r] = savedUsd - recoveryUsd * (recovered > 0 ? sum / recovered : 0) - spotlightUsd;
|
|
307
|
+
for (let k = 0; k < toolSlot.length; k++) excess[k][r] = toolSlot[k] < 0 ? 0 : toolExcess[toolSlot[k]];
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
total: percentileInterval(total),
|
|
311
|
+
net: percentileInterval(net),
|
|
312
|
+
netUsd: percentileInterval(netUsd),
|
|
313
|
+
excess: excess.map(samples => percentileInterval(samples)),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Ranking quality
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
type RankingGroup = SpotlightRankingCell["group"];
|
|
322
|
+
const RANKING_GROUPS: readonly RankingGroup[] = ["applied", "holdout", "scoreFree"];
|
|
323
|
+
|
|
324
|
+
/** A shaped read's ranking group; null for a cut the no-score fallback made because Spotlight's calls failed. */
|
|
325
|
+
function rankingGroup(result: Result): RankingGroup | null {
|
|
326
|
+
if (result.holdout) return "holdout";
|
|
327
|
+
if (!result.shortened) return null;
|
|
328
|
+
if (result.scoreFree) return "scoreFree";
|
|
329
|
+
return result.failed ? null : "applied";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** A line's heat digit (0–9); 0 for a line Spotlight never scored. */
|
|
333
|
+
function heatDigit(heat: string, i: number): number {
|
|
334
|
+
const c = heat.charCodeAt(i);
|
|
335
|
+
return c >= 48 && c <= 57 ? c - 48 : 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Per-session sums of one ranking cell (ratios of sums bootstrap over sessions). */
|
|
339
|
+
interface RankingSums {
|
|
340
|
+
results: number;
|
|
341
|
+
neededLines: number;
|
|
342
|
+
aucSum: number;
|
|
343
|
+
aucCount: number;
|
|
344
|
+
topObserved: number;
|
|
345
|
+
topExpected: number;
|
|
346
|
+
keptObserved: number;
|
|
347
|
+
keptExpected: number;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** A mask over positions 1..n of the given 1-based ranges (index = position). */
|
|
351
|
+
function lineMask(n: number, ranges: readonly [number, number][]): Uint8Array {
|
|
352
|
+
const mask = new Uint8Array(n + 1);
|
|
353
|
+
for (const [a, b] of ranges) mask.fill(1, Math.max(1, a), Math.min(n, b) + 1);
|
|
354
|
+
return mask;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function emptyRanking(): SpotlightRanking {
|
|
358
|
+
return { cells: [], position: [], sources: { reread: 0, edit: 0 } };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const POSITION_LABELS: readonly string[] = [
|
|
362
|
+
"line 1",
|
|
363
|
+
"lines 2–3",
|
|
364
|
+
...Array.from({ length: 10 }, (_, d) => `${d * 10}–${(d + 1) * 10}%`),
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Ranking quality per group over shaped reads with needed lines (see {@link SpotlightRankingCell}), mean heat by
|
|
369
|
+
* line position over every scored cut or would-be cut, and how the needs were observed.
|
|
370
|
+
*/
|
|
371
|
+
export function computeRanking(evaluated: readonly Evaluated[]): SpotlightRanking {
|
|
372
|
+
const cells = new Map<RankingGroup, Map<string, RankingSums>>();
|
|
373
|
+
const sources = { reread: 0, edit: 0 };
|
|
374
|
+
const positionSum = new Float64Array(POSITION_LABELS.length);
|
|
375
|
+
const positionLines = new Float64Array(POSITION_LABELS.length);
|
|
376
|
+
for (const { result, analysis } of evaluated) {
|
|
377
|
+
const n = result.lines;
|
|
378
|
+
if ((result.shortened || result.holdout) && !result.scoreFree && n >= POSITION_MIN_LINES) {
|
|
379
|
+
const m = Math.min(n, HEAT_LINES);
|
|
380
|
+
for (let i = 0; i < Math.min(m, result.heat.length); i++) {
|
|
381
|
+
const c = result.heat.charCodeAt(i);
|
|
382
|
+
if (c < 48 || c > 57) continue;
|
|
383
|
+
const slot = i === 0 ? 0 : i <= 2 ? 1 : 2 + Math.min(9, Math.floor((10 * i) / m));
|
|
384
|
+
positionSum[slot] += c - 48;
|
|
385
|
+
positionLines[slot]++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const group = rankingGroup(result);
|
|
389
|
+
if (result.tool !== "read" || n <= 0 || !result.needs || !group) continue;
|
|
390
|
+
const needed = lineMask(n, mergeRanges([...result.needs.reread, ...result.needs.edit]));
|
|
391
|
+
let neededCount = 0;
|
|
392
|
+
for (let p = 1; p <= n; p++) neededCount += needed[p];
|
|
393
|
+
if (neededCount === 0) continue;
|
|
394
|
+
if (result.needs.reread.some(([a, b]) => a <= n && b >= 1)) sources.reread++;
|
|
395
|
+
if (result.needs.edit.some(([a, b]) => a <= n && b >= 1)) sources.edit++;
|
|
396
|
+
|
|
397
|
+
let bySession = cells.get(group);
|
|
398
|
+
if (!bySession) {
|
|
399
|
+
bySession = new Map();
|
|
400
|
+
cells.set(group, bySession);
|
|
401
|
+
}
|
|
402
|
+
const file = analysis.session.session_file;
|
|
403
|
+
let sums = bySession.get(file);
|
|
404
|
+
if (!sums) {
|
|
405
|
+
sums = {
|
|
406
|
+
results: 0,
|
|
407
|
+
neededLines: 0,
|
|
408
|
+
aucSum: 0,
|
|
409
|
+
aucCount: 0,
|
|
410
|
+
topObserved: 0,
|
|
411
|
+
topExpected: 0,
|
|
412
|
+
keptObserved: 0,
|
|
413
|
+
keptExpected: 0,
|
|
414
|
+
};
|
|
415
|
+
bySession.set(file, sums);
|
|
416
|
+
}
|
|
417
|
+
sums.results++;
|
|
418
|
+
sums.neededLines += neededCount;
|
|
419
|
+
|
|
420
|
+
const kept = lineMask(n, result.kept);
|
|
421
|
+
let keptCount = 0;
|
|
422
|
+
let neededKept = 0;
|
|
423
|
+
for (let p = 1; p <= n; p++) {
|
|
424
|
+
keptCount += kept[p];
|
|
425
|
+
neededKept += kept[p] & needed[p];
|
|
426
|
+
}
|
|
427
|
+
sums.keptObserved += neededKept;
|
|
428
|
+
sums.keptExpected += (neededCount * keptCount) / n;
|
|
429
|
+
|
|
430
|
+
if (group === "scoreFree") continue;
|
|
431
|
+
const m = Math.min(n, HEAT_LINES);
|
|
432
|
+
const all = new Float64Array(10);
|
|
433
|
+
const need = new Float64Array(10);
|
|
434
|
+
for (let i = 0; i < m; i++) {
|
|
435
|
+
const d = heatDigit(result.heat, i);
|
|
436
|
+
all[d]++;
|
|
437
|
+
need[d] += needed[i + 1];
|
|
438
|
+
}
|
|
439
|
+
let neededIn = 0;
|
|
440
|
+
for (const count of need) neededIn += count;
|
|
441
|
+
const unneededIn = m - neededIn;
|
|
442
|
+
if (neededIn > 0 && unneededIn > 0) {
|
|
443
|
+
let below = 0;
|
|
444
|
+
let concordant = 0;
|
|
445
|
+
for (let d = 0; d < 10; d++) {
|
|
446
|
+
const unneeded = all[d] - need[d];
|
|
447
|
+
concordant += need[d] * below + 0.5 * need[d] * unneeded;
|
|
448
|
+
below += unneeded;
|
|
449
|
+
}
|
|
450
|
+
sums.aucSum += concordant / (neededIn * unneededIn);
|
|
451
|
+
sums.aucCount++;
|
|
452
|
+
}
|
|
453
|
+
if (neededIn > 0) {
|
|
454
|
+
// The hottest k lines; ties at the boundary digit share the remaining places evenly.
|
|
455
|
+
const k = Math.ceil(TOP_SHARE * m);
|
|
456
|
+
let remaining = k;
|
|
457
|
+
let observed = 0;
|
|
458
|
+
for (let d = 9; d >= 0 && remaining > 0; d--) {
|
|
459
|
+
const take = Math.min(remaining, all[d]);
|
|
460
|
+
if (take > 0) observed += (take * need[d]) / all[d];
|
|
461
|
+
remaining -= take;
|
|
462
|
+
}
|
|
463
|
+
sums.topObserved += observed;
|
|
464
|
+
sums.topExpected += (k * neededIn) / m;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const out: SpotlightRankingCell[] = RANKING_GROUPS.map(group => {
|
|
469
|
+
const sessions = [...(cells.get(group)?.values() ?? [])];
|
|
470
|
+
const scored = group !== "scoreFree";
|
|
471
|
+
return {
|
|
472
|
+
group,
|
|
473
|
+
results: sessions.reduce((sum, s) => sum + s.results, 0),
|
|
474
|
+
neededLines: sessions.reduce((sum, s) => sum + s.neededLines, 0),
|
|
475
|
+
auc: scored
|
|
476
|
+
? bootstrapRatio(
|
|
477
|
+
sessions.map(s => [s.aucSum, s.aucCount]),
|
|
478
|
+
`ranking:${group}:auc`,
|
|
479
|
+
)
|
|
480
|
+
: NO_ESTIMATE,
|
|
481
|
+
topLift: scored
|
|
482
|
+
? bootstrapRatio(
|
|
483
|
+
sessions.map(s => [s.topObserved, s.topExpected]),
|
|
484
|
+
`ranking:${group}:top`,
|
|
485
|
+
)
|
|
486
|
+
: NO_ESTIMATE,
|
|
487
|
+
keptLift: bootstrapRatio(
|
|
488
|
+
sessions.map(s => [s.keptObserved, s.keptExpected]),
|
|
489
|
+
`ranking:${group}:kept`,
|
|
490
|
+
),
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
const position: SpotlightHeatPosition[] = POSITION_LABELS.map((label, slot) => ({
|
|
494
|
+
label,
|
|
495
|
+
meanHeat: positionLines[slot] > 0 ? positionSum[slot] / positionLines[slot] : 0,
|
|
496
|
+
lines: positionLines[slot],
|
|
497
|
+
}));
|
|
498
|
+
return { cells: out, position, sources };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Step economics
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
function sizeBin(savedTokens: number): SpotlightSizeBin {
|
|
506
|
+
return (SIZE_BINS.find(({ below }) => savedTokens < below) ?? SIZE_BINS[SIZE_BINS.length - 1]).bin;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
interface StepSums {
|
|
510
|
+
tool: SpotlightTool;
|
|
511
|
+
steps: string;
|
|
512
|
+
size: SpotlightSizeBin;
|
|
513
|
+
cuts: number;
|
|
514
|
+
savedTokens: number;
|
|
515
|
+
recoveredTokens: number;
|
|
516
|
+
/** Σ each cut's holdout baseline: what its cuts get fetched back anyway. */
|
|
517
|
+
anyway: number;
|
|
518
|
+
/** Per session: [fetched, measured] (cuts with a recorded call). */
|
|
519
|
+
fetchBacks: Map<string, [number, number]>;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* What each step combination earns per tool and saved-token bin over the range's cuts; combinations with
|
|
524
|
+
* under {@link MIN_STEP_CUTS} cuts of a tool fold into `other`. The excess subtracts each cut's holdout baseline
|
|
525
|
+
* ({@link holdoutBaselines}). Cells follow `tools`' order, then the most common combination first.
|
|
526
|
+
*/
|
|
527
|
+
export function computeSteps(
|
|
528
|
+
evaluated: readonly Evaluated[],
|
|
529
|
+
baselineOf: (tool: SpotlightTool) => number,
|
|
530
|
+
tools: readonly SpotlightTool[],
|
|
531
|
+
): SpotlightStepCell[] {
|
|
532
|
+
const cuts = evaluated.filter(item => item.result.shortened);
|
|
533
|
+
const combos = new Map<string, number>();
|
|
534
|
+
const comboOf = (item: Evaluated) => `${item.tool}\0${item.result.steps.join("+") || "none"}`;
|
|
535
|
+
for (const item of cuts) combos.set(comboOf(item), (combos.get(comboOf(item)) ?? 0) + 1);
|
|
536
|
+
const cells = new Map<string, StepSums>();
|
|
537
|
+
for (const item of cuts) {
|
|
538
|
+
const combo = comboOf(item);
|
|
539
|
+
const steps = (combos.get(combo) ?? 0) >= MIN_STEP_CUTS ? combo.slice(item.tool.length + 1) : "other";
|
|
540
|
+
const size = sizeBin(item.result.savedTokens);
|
|
541
|
+
const key = `${item.tool}\0${steps}\0${size}`;
|
|
542
|
+
let cell = cells.get(key);
|
|
543
|
+
if (!cell) {
|
|
544
|
+
cell = {
|
|
545
|
+
tool: item.tool,
|
|
546
|
+
steps,
|
|
547
|
+
size,
|
|
548
|
+
cuts: 0,
|
|
549
|
+
savedTokens: 0,
|
|
550
|
+
recoveredTokens: 0,
|
|
551
|
+
anyway: 0,
|
|
552
|
+
fetchBacks: new Map(),
|
|
553
|
+
};
|
|
554
|
+
cells.set(key, cell);
|
|
555
|
+
}
|
|
556
|
+
cell.cuts++;
|
|
557
|
+
cell.savedTokens += item.result.savedTokens;
|
|
558
|
+
cell.recoveredTokens += recoveredTokens(item.call);
|
|
559
|
+
cell.anyway += baselineOf(item.tool);
|
|
560
|
+
if (Number.isFinite(item.call.seq)) {
|
|
561
|
+
const file = item.analysis.session.session_file;
|
|
562
|
+
const pair = cell.fetchBacks.get(file) ?? [0, 0];
|
|
563
|
+
if (item.call.fetchBackGap !== null) pair[0]++;
|
|
564
|
+
pair[1]++;
|
|
565
|
+
cell.fetchBacks.set(file, pair);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const toolIndex = new Map(tools.map((tool, index) => [tool, index]));
|
|
569
|
+
const toolOrder = (tool: SpotlightTool) => toolIndex.get(tool) ?? tools.length;
|
|
570
|
+
const comboCuts = (cell: StepSums) =>
|
|
571
|
+
cell.steps === "other" ? -1 : (combos.get(`${cell.tool}\0${cell.steps}`) ?? 0);
|
|
572
|
+
const sizeOrder = (size: SpotlightSizeBin) => SIZE_BINS.findIndex(entry => entry.bin === size);
|
|
573
|
+
return [...cells.values()]
|
|
574
|
+
.sort(
|
|
575
|
+
(a, b) =>
|
|
576
|
+
toolOrder(a.tool) - toolOrder(b.tool) ||
|
|
577
|
+
comboCuts(b) - comboCuts(a) ||
|
|
578
|
+
a.steps.localeCompare(b.steps) ||
|
|
579
|
+
sizeOrder(a.size) - sizeOrder(b.size),
|
|
580
|
+
)
|
|
581
|
+
.map(cell => {
|
|
582
|
+
const excess = Math.max(0, cell.recoveredTokens - cell.anyway);
|
|
583
|
+
return {
|
|
584
|
+
tool: cell.tool,
|
|
585
|
+
steps: cell.steps,
|
|
586
|
+
size: cell.size,
|
|
587
|
+
cuts: cell.cuts,
|
|
588
|
+
fetchBackRate: bootstrapRatio(
|
|
589
|
+
[...cell.fetchBacks.values()],
|
|
590
|
+
`steps:${cell.tool}:${cell.steps}:${cell.size}`,
|
|
591
|
+
),
|
|
592
|
+
savedTokens: cell.savedTokens,
|
|
593
|
+
recoveredTokens: cell.recoveredTokens,
|
|
594
|
+
excessTokens: excess,
|
|
595
|
+
keptShare: cell.savedTokens > 0 ? (cell.savedTokens - excess) / cell.savedTokens : null,
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Time cost and coverage
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Agent time of the experiment sessions in the range: the model time of their requests, of the requests that did
|
|
606
|
+
* nothing but fetch back cut output, and the time tool calls spent shaping.
|
|
607
|
+
*/
|
|
608
|
+
export function computeTimeCost(active: readonly SessionAnalysis[], cutoff: number): SpotlightTimeCost {
|
|
609
|
+
const cell: SpotlightTimeCost = { agentMs: 0, fetchBackTurns: 0, fetchBackMs: 0, shapeMs: 0, share: null };
|
|
610
|
+
for (const analysis of active) {
|
|
611
|
+
const requests = new Map(analysis.requests.map(request => [request.entryId, request]));
|
|
612
|
+
for (const request of analysis.requests) if (request.timestamp >= cutoff) cell.agentMs += request.durationMs;
|
|
613
|
+
const fetchBackCalls = new Map<string, { calls: number; callsInTurn: number }>();
|
|
614
|
+
for (const call of analysis.calls) {
|
|
615
|
+
const result = call.result;
|
|
616
|
+
if (result && result.timestamp >= cutoff) cell.shapeMs += result.shapeMs ?? 0;
|
|
617
|
+
if (!call.entryId || !call.recovery?.source.result?.shortened) continue;
|
|
618
|
+
const entry = fetchBackCalls.get(call.entryId) ?? { calls: 0, callsInTurn: call.callsInTurn };
|
|
619
|
+
entry.calls++;
|
|
620
|
+
fetchBackCalls.set(call.entryId, entry);
|
|
621
|
+
}
|
|
622
|
+
for (const [entryId, entry] of fetchBackCalls) {
|
|
623
|
+
const request = requests.get(entryId);
|
|
624
|
+
if (entry.calls !== entry.callsInTurn || !request || request.timestamp < cutoff) continue;
|
|
625
|
+
cell.fetchBackTurns++;
|
|
626
|
+
cell.fetchBackMs += request.durationMs;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
cell.share = cell.agentMs > 0 ? (cell.fetchBackMs + cell.shapeMs) / cell.agentMs : null;
|
|
630
|
+
return cell;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Each tool's share of the tool output experiment sessions' models saw from their marker (and the range) on;
|
|
635
|
+
* `shaped` marks the tools with evaluated results in the range.
|
|
636
|
+
*/
|
|
637
|
+
export function computeCoverage(
|
|
638
|
+
database: Database,
|
|
639
|
+
active: readonly SessionAnalysis[],
|
|
640
|
+
cutoff: number,
|
|
641
|
+
shaped: ReadonlySet<SpotlightTool>,
|
|
642
|
+
): SpotlightCoverageRow[] {
|
|
643
|
+
const query = database.query<{ tool_name: string; calls: number; chars: number }, [string, number]>(
|
|
644
|
+
`SELECT tool_name, COUNT(*) AS calls, TOTAL(result_chars) AS chars
|
|
645
|
+
FROM tool_calls WHERE session_file = ? AND timestamp >= ? GROUP BY tool_name`,
|
|
646
|
+
);
|
|
647
|
+
const byTool = new Map<string, { calls: number; chars: number }>();
|
|
648
|
+
for (const analysis of active) {
|
|
649
|
+
for (const row of query.all(analysis.session.session_file, Math.max(cutoff, analysis.session.marked_at))) {
|
|
650
|
+
const entry = byTool.get(row.tool_name) ?? { calls: 0, chars: 0 };
|
|
651
|
+
entry.calls += row.calls;
|
|
652
|
+
entry.chars += row.chars;
|
|
653
|
+
byTool.set(row.tool_name, entry);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
let total = 0;
|
|
657
|
+
for (const entry of byTool.values()) total += entry.chars;
|
|
658
|
+
return [...byTool]
|
|
659
|
+
.map(([tool, entry]) => ({
|
|
660
|
+
tool,
|
|
661
|
+
shaped: shaped.has(tool),
|
|
662
|
+
calls: entry.calls,
|
|
663
|
+
tokens: Math.round(entry.chars / CHARS_PER_TOKEN),
|
|
664
|
+
share: total > 0 ? entry.chars / total : 0,
|
|
665
|
+
}))
|
|
666
|
+
.sort((a, b) => b.tokens - a.tokens || a.tool.localeCompare(b.tool));
|
|
667
|
+
}
|