Prioritize investigation hypotheses by confidence after bootstrap

This commit is contained in:
Nico committed 2026-10-15 15:16:38 +02:00
1 parent f4082585ae
commit c44ccd5ce3
5 files changed
+128 -2

No files matched your search

+35
View File
@@ -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.
@@ -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)]
@@ -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")
+39
View File
@@ -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']
+24
View File
@@ -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