@@ -274,6 +274,24 @@ def _tokens_to_first_correct(context: Dict[str, Any], expected_symbols: Sequence
274274 return None
275275
276276
277+ def _tokens_to_full_change_surface (context : Dict [str , Any ], expected_symbols : Sequence [str ]) -> Optional [int ]:
278+ if not expected_symbols :
279+ return None
280+
281+ expected_set = set (expected_symbols )
282+ seen : set [str ] = set ()
283+ token_total = 0
284+ for section in context .get ("sections" , []):
285+ token_total += int (section .get ("token_count" , retrieval ._approx_token_count (section ["content" ])))
286+ if section .get ("title" ) != "Primary" :
287+ continue
288+ if section ["symbol" ] in expected_set :
289+ seen .add (section ["symbol" ])
290+ if seen >= expected_set :
291+ return token_total
292+ return None
293+
294+
277295def _status_for_case (case : Dict [str , Any ]) -> str :
278296 if case ["query_type" ] == "no_answer" :
279297 return "ignored" if case ["no_answer_correct" ] else "false_positive"
@@ -294,11 +312,23 @@ def _summarize_cases(cases: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
294312 token_counts = [case ["token_count" ] for case in cases ]
295313 retrieved_context_total = sum (case ["retrieved_context_count" ] for case in positive_cases )
296314 relevant_context_total = sum (case ["relevant_context_count" ] for case in positive_cases )
315+ anchor_set_recall_values = [case ["anchor_set_recall" ] for case in positive_cases if case ["anchor_set_recall" ] is not None ]
316+ surface_bundle_recall_values = [
317+ case ["surface_bundle_recall" ]
318+ for case in positive_cases
319+ if case ["surface_bundle_recall" ] is not None
320+ ]
297321 tokens_to_first_correct = [
298322 case ["tokens_to_first_correct" ]
299323 for case in positive_cases
300324 if case ["tokens_to_first_correct" ] is not None
301325 ]
326+ multi_anchor_cases = [case for case in positive_cases if len (case ["expected_symbols" ]) > 1 ]
327+ tokens_to_full_change_surface = [
328+ case ["tokens_to_full_change_surface" ]
329+ for case in multi_anchor_cases
330+ if case ["tokens_to_full_change_surface" ] is not None
331+ ]
302332
303333 return {
304334 "query_count" : len (cases ),
@@ -313,6 +343,17 @@ def _summarize_cases(cases: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
313343 ),
314344 "false_positive_rate" : _ratio (sum (1 for case in cases if case ["false_positive" ]), len (cases )),
315345 "average_tokens_to_first_correct" : _mean (tokens_to_first_correct ),
346+ "anchor_set_recall" : round (mean (anchor_set_recall_values ), 4 ) if anchor_set_recall_values else None ,
347+ "surface_bundle_recall" : round (mean (surface_bundle_recall_values ), 4 ) if surface_bundle_recall_values else None ,
348+ "full_change_surface_rate" : _ratio (
349+ sum (1 for case in multi_anchor_cases if case ["full_change_surface_found" ]),
350+ len (multi_anchor_cases ),
351+ ),
352+ "paired_surface_top_hit" : _ratio (
353+ sum (1 for case in multi_anchor_cases if case ["paired_surface_top_hit" ]),
354+ len (multi_anchor_cases ),
355+ ),
356+ "average_tokens_to_full_change_surface" : _mean (tokens_to_full_change_surface ),
316357 "top1_accuracy" : _ratio (sum (1 for case in cases if case ["top1_correct" ]), len (cases )),
317358 "topk_accuracy" : _ratio (sum (1 for case in cases if case ["topk_exact" ]), len (cases )),
318359 "definition_hit_rate" : _ratio (
@@ -411,6 +452,20 @@ def _run_engine_case(
411452 no_answer_correct = expected_list == [] and context_symbols == []
412453 relevant_context_count = sum (1 for symbol in context_symbols if symbol in expected_set )
413454 retrieved_context_count = len (context_symbols )
455+ surface_bundle_symbols = [item ["symbol" ] for item in plan .get ("surface_bundle" , [])]
456+ surface_bundle_relevant_count = len (set (surface_bundle_symbols ) & expected_set )
457+ anchor_set_recall = (
458+ round (relevant_context_count / len (expected_list ), 4 )
459+ if expected_list
460+ else None
461+ )
462+ surface_bundle_recall = (
463+ round (surface_bundle_relevant_count / len (expected_list ), 4 )
464+ if expected_list
465+ else None
466+ )
467+ full_change_surface_found = bool (expected_list ) and all (symbol in context_symbols for symbol in expected_list )
468+ paired_surface_top_hit = bool (len (expected_list ) > 1 and context_symbols [:1 ] == expected_list [:1 ] and expected_list [1 ] in context_symbols )
414469
415470 return {
416471 "query" : query ,
@@ -429,12 +484,18 @@ def _run_engine_case(
429484 if expected_list and retrieved_context_count
430485 else None
431486 ),
487+ "anchor_set_recall" : anchor_set_recall ,
488+ "surface_bundle_recall" : surface_bundle_recall ,
489+ "full_change_surface_found" : full_change_surface_found ,
490+ "paired_surface_top_hit" : paired_surface_top_hit ,
432491 "tokens_to_first_correct" : _tokens_to_first_correct (context , expected_list ),
492+ "tokens_to_full_change_surface" : _tokens_to_full_change_surface (context , expected_list ),
433493 "false_positive" : retrieved_context_count > 0 and relevant_context_count == 0 ,
434494 "token_count" : context ["token_count" ],
435495 "line_count" : context ["line_count" ],
436496 "truncated" : context ["truncated" ],
437497 "plan_symbols" : [entry ["symbol" ] for entry in plan .get ("entries" , [])],
498+ "surface_bundle_symbols" : surface_bundle_symbols ,
438499 }
439500
440501
@@ -530,21 +591,26 @@ def render_fixture_benchmark_markdown(report: Dict[str, Any]) -> str:
530591 "" ,
531592 "## Aggregate" ,
532593 "" ,
533- "| Engine | Budget | Queries | Precision | Recall | False positives | Tokens to first correct | Top-1 | Top-K | Caller | Trace | No-answer | Avg context tokens |" ,
534- "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |" ,
594+ "| Engine | Budget | Queries | Precision | Recall | Anchor recall | Surface bundle | False positives | Tokens to first correct | Tokens to full set | Full change set | Paired top | Top-1 | Top-K | Caller | Trace | No-answer | Avg context tokens |" ,
595+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | " ,
535596 ]
536597 for engine_name in report ["config" ]["engines" ]:
537598 for budget in report ["config" ]["token_budgets" ]:
538599 summary = report ["engines" ][engine_name ]["budgets" ][str (budget )]["summary" ]
539600 lines .append (
540- "| {engine} | {budget} | {query_count} | {precision} | {recall} | {false_positive} | {first_correct_tokens} | {top1} | {topk} | {caller} | {trace} | {no_answer} | {tokens} |" .format (
601+ "| {engine} | {budget} | {query_count} | {precision} | {recall} | {anchor_recall} | {bundle_recall} | { false_positive} | {first_correct_tokens} | {full_surface_tokens} | {full_surface_rate} | {paired_top } | {top1} | {topk} | {caller} | {trace} | {no_answer} | {tokens} |" .format (
541602 engine = ENGINE_LABELS .get (engine_name , engine_name ),
542603 budget = budget ,
543604 query_count = summary ["query_count" ],
544605 precision = _format_metric (summary ["context_precision" ], percent = True ),
545606 recall = _format_metric (summary ["context_recall" ], percent = True ),
607+ anchor_recall = _format_metric (summary ["anchor_set_recall" ], percent = True ),
608+ bundle_recall = _format_metric (summary ["surface_bundle_recall" ], percent = True ),
546609 false_positive = _format_metric (summary ["false_positive_rate" ], percent = True ),
547610 first_correct_tokens = _format_metric (summary ["average_tokens_to_first_correct" ]),
611+ full_surface_tokens = _format_metric (summary ["average_tokens_to_full_change_surface" ]),
612+ full_surface_rate = _format_metric (summary ["full_change_surface_rate" ], percent = True ),
613+ paired_top = _format_metric (summary ["paired_surface_top_hit" ], percent = True ),
548614 top1 = _format_metric (summary ["top1_accuracy" ], percent = True ),
549615 topk = _format_metric (summary ["topk_accuracy" ], percent = True ),
550616 caller = _format_metric (summary ["caller_hit_rate" ], percent = True ),
0 commit comments