From c44ccd5ce37861e816c31fb84b73645631064cd2 Mon Sep 17 00:00:00 2001 From: Nitsu45 Date: Thu, 15 Oct 2026 15:16:38 +0200 Subject: [PATCH] Prioritize investigation hypotheses by confidence after bootstrap --- docs/investigation-scheduling-v1.md | 35 +++++++++++++++++ .../analysis/investigation_scheduler.py | 27 +++++++++++++ .../analysis/investigator.py | 5 ++- tests/test_investigation_scheduler.py | 39 +++++++++++++++++++ tests/test_investigator.py | 24 ++++++++++++ 5 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 docs/investigation-scheduling-v1.md create mode 100644 src/viano_bug_analysis/analysis/investigation_scheduler.py create mode 100644 tests/test_investigation_scheduler.py diff --git a/docs/investigation-scheduling-v1.md b/docs/investigation-scheduling-v1.md new file mode 100644 index 0000000..6a5e737 --- /dev/null +++ b/docs/investigation-scheduling-v1.md @@ -0,0 +1,35 @@ +# Confidence-prioritized investigation scheduling + +The controller gives every active, eligible hypothesis one bootstrap turn in +numeric ID order. After that it chooses by qualitative confidence: +strongly_supported > supported > plausible > unassessed > weak > weakened. +Equal levels use numeric hypothesis ID (H1 before H2 before H10), not rotation. +These ranks are ordering values, not probabilities or a new model-owned budget. + +Selection is recomputed between turns. A strengthening hypothesis can continue; +a weakened hypothesis yields to a higher-ranked alternative without losing its +remaining budget or evidence. Initial turns generally issue queries: their results +are semantically assessed on a subsequent turn, so the bootstrap does not claim +that all hypotheses are already evaluated after three calls. + +Existing eligibility checks, positive action costs, rewards, iteration safeguards, +terminal dispositions and final pending-result assessments remain in force. An +interrupted/pending active turn resumes as before; it is not preempted or rewritten. +Existing cases adopt the scheduler at their next selection boundary. Archived +states, contexts and model request hashes are not changed. The historical cursor +field is retained for file compatibility but no longer determines selection. + +## Completion remains separate from selection + +The model may finish its selected hypothesis with remaining credit, or reject it. +Supported confidence requires stored evidence. This does not itself globally +finish the case. The controller transitions to BUILDING_DIAGNOSIS after all +hypotheses have terminal accounts and pending results have been handled, whether +through model finish/reject, budget exhaustion or controller limits. No rule +requires spending every allocated unit. A weak hypothesis is deprioritized, not +automatically rejected just because another ranks higher. + +The final diagnosis evaluates all alternatives, root-cause evidence and adequacy +of a countercheck. Only an accepted supported diagnosis can proceed to a coding +change request; inconclusive cases go to review. An automatic global early finish +on a single high-confidence declaration is not introduced by this scheduler. diff --git a/src/viano_bug_analysis/analysis/investigation_scheduler.py b/src/viano_bug_analysis/analysis/investigation_scheduler.py new file mode 100644 index 0000000..b489182 --- /dev/null +++ b/src/viano_bug_analysis/analysis/investigation_scheduler.py @@ -0,0 +1,27 @@ +"""Deterministic scheduling, independent of model calls and budget accounting.""" + +CONFIDENCE_ORDER = { + 'strongly_supported': 5, + 'supported': 4, + 'plausible': 3, + 'unassessed': 2, + 'weak': 1, + 'weakened': 0, +} + + +def priority_order(hypotheses): + """One bootstrap turn each, then confidence with numeric ID tie-breaking. + + Eligibility, pending-action checks and iteration/budget limits remain the + controller's responsibility. An interrupted active turn is never rescheduled. + These ranks order qualitative levels; they are not confidence probabilities. + """ + ids = sorted(hypotheses, key=lambda hid:int(hid[1:])) + positions = {hid:index for index,hid in enumerate(ids)} + def rank(hid): + hypothesis = hypotheses[hid] + return (0 if hypothesis['iterations']==0 else 1, + 0 if hypothesis['iterations']==0 else -CONFIDENCE_ORDER[hypothesis['confidence']], + int(hid[1:])) + return [(positions[hid],hid) for hid in sorted(ids,key=rank)] diff --git a/src/viano_bug_analysis/analysis/investigator.py b/src/viano_bug_analysis/analysis/investigator.py index 11347c2..230a3dd 100644 --- a/src/viano_bug_analysis/analysis/investigator.py +++ b/src/viano_bug_analysis/analysis/investigator.py @@ -239,8 +239,9 @@ class Investigator: snapshot=budget.snapshot(case_id,package_id) ids=sorted(state["hypotheses"],key=lambda value:int(value[1:])) selected=None - for offset in range(len(ids)): - pos=(state["cursor"]+offset)%len(ids);hid=ids[pos];h=state["hypotheses"][hid];account=snapshot.state["accounts"][hid] + from .investigation_scheduler import priority_order + for pos,hid in priority_order(state['hypotheses']): + h=state["hypotheses"][hid];account=snapshot.state["accounts"][hid] if h["disposition"]!="active":continue if any(a["status"]!="settled" for a in account["actions"].values()) and h["last_record_ref"] is None: raise InvestigationError("INVESTIGATION_UNOWNED_PENDING_ACTION") diff --git a/tests/test_investigation_scheduler.py b/tests/test_investigation_scheduler.py new file mode 100644 index 0000000..46225c1 --- /dev/null +++ b/tests/test_investigation_scheduler.py @@ -0,0 +1,39 @@ +import copy + +from viano_bug_analysis.analysis.investigation_scheduler import priority_order + + +def hypothesis(confidence='unassessed',iterations=1): + return {'confidence':confidence,'iterations':iterations} + + +def order(hypotheses): + return [hid for _,hid in priority_order(hypotheses)] + + +def test_bootstrap_turns_precede_already_strong_hypotheses(): + states={'H1':hypothesis('strongly_supported'),'H2':hypothesis(iterations=0),'H3':hypothesis(iterations=0)} + assert order(states)==['H2','H3','H1'] + states['H2']['iterations']=1 + assert order(states)==['H3','H1','H2'] + + +def test_confidence_rises_and_falls_without_changing_budget_or_state(): + states={'H1':hypothesis('plausible'),'H2':hypothesis('supported'),'H3':hypothesis('weak')} + before=copy.deepcopy(states) + assert order(states)==['H2','H1','H3'] + assert states==before + states['H2']['confidence']='weakened' + assert order(states)==['H1','H3','H2'] + states['H3']['confidence']='strongly_supported' + assert order(states)==['H3','H1','H2'] + + +def test_equal_confidence_uses_numeric_hypothesis_id_not_rotation_or_string_sort(): + states={hid:hypothesis('supported') for hid in ['H10','H5','H3','H2','H1']} + assert order(states)==['H1','H2','H3','H5','H10'] + assert dict(priority_order(states))[0]=='H1' + + +def test_unassessed_alternative_is_preferred_over_weakened_candidate(): + assert order({'H1':hypothesis('weak'),'H2':hypothesis(),'H3':hypothesis('weakened')})==['H2','H1','H3'] diff --git a/tests/test_investigator.py b/tests/test_investigator.py index 96a487c..4029889 100644 --- a/tests/test_investigator.py +++ b/tests/test_investigator.py @@ -270,3 +270,27 @@ def test_provider_configuration_error_blocks_without_abandoning_hypotheses(tool_ @pytest.mark.parametrize("name",["investigation-output","investigation-state","investigation-context"]) def test_loop_schemas_valid(name): Draft202012Validator.check_schema(json.loads(files("viano_bug_analysis.schemas").joinpath(name+'-v1.schema.json').read_text())) + + +def test_confidence_scheduler_keeps_growing_candidate_then_switches_after_weakening(tool_case): + def change(reply,context): + hid=context['selected_hypothesis'] + turns=sum(c['selected_hypothesis']==hid for c in adapter.contexts) + if hid=='H1' and turns in (2,3): + reply['update']['confidence']='supported' if turns==2 else 'weakened' + reply['next']={'kind':'tool','operation':'source.read_context', + 'arguments':{'source_ref':'tool/code.cpp','start_line':1,'end_line':2}} + adapter=LoopAdapter(change) + loop=Investigator(tool_case[0],gateway(adapter)) + first=run(loop,steps=4) + assert first.status=='YIELDED' + assert [c['selected_hypothesis'] for c in adapter.contexts]==['H1','H2','H3','H1'] + # Resume the persisted confidence state; neither cursor nor a restart forces rotation. + result=run(Investigator(tool_case[0],gateway(adapter)),steps=2) + assert [c['selected_hypothesis'] for c in adapter.contexts]==['H1','H2','H3','H1','H1','H2'] + saved=state(result) + assert saved['hypotheses']['H1']['confidence']=='weakened' + assert saved['hypotheses']['H1']['disposition']=='active' + account=BudgetManager(tool_case[0]).snapshot(*CASE).state['accounts']['H1'] + assert account['remaining']>0 and account['status']=='investigating' + assert PackageValidator().validate(result.package).valid