1111// edited — stops that fired on a run which had already edited files.
1212// early — stops that fired before half the turn budget was spent.
1313//
14- // Also aggregates outcome records (CL-6938) : what each completed dispatch
15- // actually produced (a salvage kind, or clean-complete), by kind. This is the
16- // log's only outcome signal, letting a stop record be read alongside what the
14+ // Also aggregates outcome records: what each completed dispatch actually
15+ // produced (a salvage kind, or clean-complete), by kind. This is the log's
16+ // only outcome signal, letting a stop record be read alongside what the
1717// dispatch it touched actually produced — it is still not gate-pass or
1818// retry-success tracking.
1919//
20+ // Outcome records are now tagged with the dispatched child's provider/model/
21+ // family (CL-6968), so they double as the per-model dispatch denominator:
22+ // interventions per model, divided by dispatches per model, is the one table
23+ // below that is an actual rate. Everything else in this script stays a raw
24+ // count — do not add another table that looks like a rate without a tracked
25+ // denominator behind it (that mistake shipped once already and had to be
26+ // stripped, see CL-6968).
27+ //
2028// Run: bun run scripts/intervention-forensics.ts
2129//
2230// Prints only aggregate counts and the `detail` field's first token, never turn
@@ -88,6 +96,12 @@ findAll(root, INTERVENTION_FILE, files);
8896
8997const buckets = new Map < string , Bucket > ( ) ;
9098const outcomes = new Map < string , number > ( ) ;
99+ // Dispatch counts per model (the outcome record's denominator, CL-6968) and
100+ // intervention counts per model (stop+nudge only — block/outcome are not
101+ // leaf signals), so a rate can be computed instead of a bare count.
102+ const dispatchesByModel = new Map < string , number > ( ) ;
103+ const interventionsByModel = new Map < string , number > ( ) ;
104+ let untaggedOutcomes = 0 ;
91105let records = 0 ;
92106let malformed = 0 ;
93107
@@ -115,6 +129,12 @@ for (const file of files) {
115129 if ( record . class === "outcome" && record . outcome !== undefined ) {
116130 const kind = record . outcome . kind ;
117131 outcomes . set ( kind , ( outcomes . get ( kind ) ?? 0 ) + 1 ) ;
132+ if ( record . model !== undefined ) {
133+ dispatchesByModel . set ( record . model , ( dispatchesByModel . get ( record . model ) ?? 0 ) + 1 ) ;
134+ } else {
135+ // Written before CL-6968 tagged outcome records with model identity.
136+ untaggedOutcomes ++ ;
137+ }
118138 continue ;
119139 }
120140 const key = `${ record . class ?? "?" } /${ record . id } ` ;
@@ -128,6 +148,9 @@ for (const file of files) {
128148 bucket . byFamily . set ( family , ( bucket . byFamily . get ( family ) ?? 0 ) + 1 ) ;
129149 const model = record . model ?? "unknown" ;
130150 bucket . byModel . set ( model , ( bucket . byModel . get ( model ) ?? 0 ) + 1 ) ;
151+ if ( record . class === "stop" || record . class === "nudge" ) {
152+ interventionsByModel . set ( model , ( interventionsByModel . get ( model ) ?? 0 ) + 1 ) ;
153+ }
131154 if ( record . measurement !== undefined ) {
132155 bucket . values . push ( record . measurement . value ) ;
133156 if ( record . measurement . threshold !== undefined ) {
@@ -178,10 +201,10 @@ for (const [key, bucket] of rows) {
178201
179202// CL-6775: streamed degenerate-repetition aborts (mid-stream, not a turn-level
180203// stop) get their own model breakdown — "repetition-<detector>" ids, one row
181- // per model, so "which model loops most" reads off directly. This is a count,
182- // not a rate normalized by dispatch volume: the log's outcome records (total
183- // completed dispatches) are written from the parent side without a model tag,
184- // so a per-model denominator is not yet tracked — see the PR description .
204+ // per model, so "which model loops most" reads off directly. Still a raw
205+ // count, not a rate — see the "interventions per dispatch by model" table
206+ // below for the rate version, computed from the same per- model dispatch
207+ // denominator (outcome records, CL-6968) .
185208const repetitionRows = rows . filter ( ( [ key ] ) => key . includes ( "/repetition-" ) ) ;
186209if ( repetitionRows . length > 0 ) {
187210 const totalsByModel = new Map < string , number > ( ) ;
@@ -209,6 +232,32 @@ console.log(
209232 "\nedited = stops on runs that had already edited files; early = stops before half the turn budget (context, not a false-positive rate)." ,
210233) ;
211234
235+ // The one real rate in this script: interventions per dispatch, per model.
236+ // dispatches = outcome records tagged with that model (CL-6968); interventions
237+ // = stop+nudge records for that model. Everything above this is a count.
238+ if ( dispatchesByModel . size > 0 || interventionsByModel . size > 0 ) {
239+ console . log ( "\ninterventions per dispatch by model (stop+nudge count / dispatch count = rate)" ) ;
240+ const models = new Set ( [ ...dispatchesByModel . keys ( ) , ...interventionsByModel . keys ( ) ] ) ;
241+ const modelRows = [ ...models ]
242+ . map ( ( model ) => {
243+ const dispatches = dispatchesByModel . get ( model ) ?? 0 ;
244+ const interventions = interventionsByModel . get ( model ) ?? 0 ;
245+ const rate = dispatches > 0 ? ( interventions / dispatches ) . toFixed ( 3 ) : "-" ;
246+ return { model, dispatches, interventions, rate } ;
247+ } )
248+ . sort ( ( a , b ) => b . interventions - a . interventions ) ;
249+ for ( const { model, dispatches, interventions, rate } of modelRows ) {
250+ console . log (
251+ `${ model . padEnd ( 33 ) } interventions=${ String ( interventions ) . padStart ( 4 ) } dispatches=${ String ( dispatches ) . padStart ( 5 ) } rate=${ rate } ` ,
252+ ) ;
253+ }
254+ if ( untaggedOutcomes > 0 ) {
255+ console . log (
256+ `(${ untaggedOutcomes } outcome record(s) predate model tagging (CL-6968) and are excluded from every dispatch count above.)` ,
257+ ) ;
258+ }
259+ }
260+
212261if ( outcomes . size > 0 ) {
213262 console . log ( "\ndispatch outcomes" ) ;
214263 const outcomeRows = [ ...outcomes . entries ( ) ] . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) ;
0 commit comments