Files
VianoBugAnalysis/src/viano_bug_analysis/analysis/investigation_lean_v20.py
T

153 lines
9.4 KiB
Python

"""Hybrid bootstrap and incremental, controller-owned consolidation bookkeeping."""
import copy
import hashlib
import json
from importlib.resources import files
from . import investigation_lean as previous
from .investigation_config import InvestigationError
VERSION='investigation_project_v20'
def legacy(context):
result=copy.deepcopy(context);result.pop('lean_protocol',None)
return result
def schema(context, *, strict=False):
result = _schema(context, strict=strict)
if context.get('assessment_scope_version') == 1:
if context.get('previous_result') is None:
result['properties']['assessment'] = {'type':'null'}
else:
result['properties']['assessment'] = next(copy.deepcopy(v) for v in
result['properties']['assessment']['anyOf'] if v.get('type') == 'object')
from .investigation_window import enabled, extend_recall
if enabled(context):
result = extend_recall(result)
if previous.checkpoint(context):
from .investigation_window import checkpoint_recall
result = checkpoint_recall(result)
return result
def _schema(context, *, strict=False):
c=legacy(context);old=previous.schema(c,strict=True)
if not previous.checkpoint(c):
branch=next(b for b in old['properties']['next']['oneOf'] if b['properties']['kind'].get('const')=='checkpoint')
branch['properties']['intent']={'enum':['completion','switch','contradiction','test','review']}
branch['required'].append('intent')
return old if strict else {**old,'properties':{**old['properties'],'next':{'type':'object','required':['kind'],
'properties':{'kind':{'enum':['tool','recall','checkpoint']}}}}}
from .investigation_compact import preparing_review
if not preparing_review(c):
old['properties']['next']['oneOf'].append({'type':'object','additionalProperties':False,
'required':['kind','hypothesis_id','reason'],'properties':{'kind':{'const':'switch'},
'hypothesis_id':{'type':'string','pattern':'^H[1-9][0-9]*$'},'reason':{'type':'string','minLength':1}}})
if 'properties' in old['properties']['next'] and 'enum' in old['properties']['next']['properties'].get('kind',{}) and not preparing_review(c):
old['properties']['next']['properties']['kind']['enum'].append('switch')
finding={'type':'object','additionalProperties':False,'required':['direction','text','evidence_refs'],
'properties':{'direction':{'enum':['support','against','neutral']},'text':{'type':'string','minLength':1,'pattern':r'\S'},
'evidence_refs':{'type':'array','minItems':1,'maxItems':8,'uniqueItems':True,
'items':{'type':'string','pattern':'^E[0-9]{6,}$'}}}}
props={'assessment':old['properties']['assessment'],'confidence':old['properties']['update']['properties']['confidence'],
'findings':{'type':'array','items':finding},'uncertainties':{'type':'array','items':{'type':'string'}},
'next_question':{'type':['string','null']},
'retire':{'type':'array','items':{'type':'object','additionalProperties':False,'required':['finding_id','reason'],
'properties':{'finding_id':{'type':'string'},'reason':{'type':'string','minLength':1,'pattern':r'\S'}}}},
'next':old['properties']['next']}
return {'type':'object','additionalProperties':False,'required':list(props),'properties':props}
def build(context):
from .model_context import model_json
c=legacy(context);request=previous.build(c);doc=json.loads(request['messages'][1]['content'])
contract=schema(context)
brief=context.get('working_brief',{}).get('brief',{})
if brief.get('next_question_details'):
if previous.checkpoint(c):doc['input']['working_brief']['next_question']=brief['next_question_details']
elif doc['input'].get('checkpoint_brief'):doc['input']['checkpoint_brief']['next_question']=brief['next_question_details']
if previous.checkpoint(c):
doc['task']=('assess_pending_result_for_early_review' if doc['task']=='assess_pending_result_for_early_review'
else 'consolidate_investigation_session')
doc['input']['checkpoint_intent']=context['lean_session'].get('intent','controller_boundary')
request['messages'][0]['content']=files('viano_bug_analysis.llm.prompts').joinpath('investigation_consolidate_v20.txt').read_text(encoding='utf-8')
else:
doc['input']['alternatives']={hid:{'title':h['proposal']['title'],'confidence':h['confidence'],'disposition':h['disposition']}
for hid,h in context['hypotheses'].items() if hid!=context['selected_hypothesis']}
doc['input']['tool_schema']['request_contract']=schema(context,strict=True)['properties']['next']
request['messages'][0]['content']=files('viano_bug_analysis.llm.prompts').joinpath('investigation_lean_v20.txt').read_text(encoding='utf-8')
doc['output_schema']=contract;request['messages'][1]['content']=model_json(doc)
request.update(prompt_version=VERSION,output_schema=contract)
return request
def chunks(text, width):
return [text[i:i+width] for i in range(0,len(text),width) if text[i:i+width].strip()]
def wire(context,reply):
"""Translate semantic additions into the frozen canonical brief contract.
Keep prior findings by default. Only explicit, justified retirements remove
them. Long text is split losslessly; it is never silently shortened.
"""
if not previous.checkpoint(context):return copy.deepcopy(reply)
old=context.get('working_brief',{}).get('brief',{}).get('findings',[])
retired={r['finding_id']:r['reason'] for r in reply['retire']}
if len(retired)!=len(reply['retire']) or set(retired)-{f['id'] for f in old}:
raise InvestigationError('INVESTIGATION_BRIEF_ID',feedback={'requirement':'retire_only_known_existing_finding_ids_once'})
findings=[copy.deepcopy(f) for f in old if f['id'] not in retired]
for f in reply['findings']:
parts=chunks(f['text'],300)
for index,part in enumerate(parts):
refs=sorted(f['evidence_refs'])
scope=('Captured query scope; see referenced evidence.' if len(parts)==1 else f'Captured query scope; text part {index+1}/{len(parts)}; see referenced evidence.')
existing=next((v for v in findings if v['direction']==f['direction'] and v['text']==part and sorted(v['evidence_refs'])==refs and (len(parts)==1 or v['scope']==scope)),None)
if existing:continue
payload=json.dumps([f['direction'],part,refs,index,len(parts)],ensure_ascii=False,separators=(',',':'))
fid='B'+str(int(hashlib.sha256(payload.encode()).hexdigest()[:20],16)).zfill(4)
if any(v['id']==fid for v in findings) or fid in retired:raise InvestigationError('INVESTIGATION_BRIEF_ID')
findings.append({'id':fid,'direction':f['direction'],'text':part,
'scope':scope,'evidence_refs':refs})
uncertainties=list(dict.fromkeys(part for text in reply['uncertainties'] for part in chunks(text,240)))
question=reply['next_question'] or 'No further question.'
if not question.strip():question='No further question.'
if len(findings)>16 or len(uncertainties)>6:
raise InvestigationError('INVESTIGATION_MEMORY_LIMIT',feedback={'requirement':'consolidate_redundant_findings_with_explicit_retirements_preserve_counterevidence'})
nxt=copy.deepcopy(reply['next'])
if nxt['kind']=='switch':
target=nxt['hypothesis_id'];h=context['hypotheses'].get(target)
from viano_bug_analysis.budget.engine import TERMINAL
if (target==context['selected_hypothesis'] or h is None or h['disposition']!='active'
or context['budget']['accounts'][target]['status'] in TERMINAL):
raise InvestigationError('INVESTIGATION_SWITCH_TARGET',feedback={'requirement':'choose_another_active_hypothesis_with_available_budget'})
nxt={'kind':'defer','reason':nxt['reason']}
return {'assessment':copy.deepcopy(reply['assessment']),'next':nxt,
'update':{'confidence':reply['confidence'],'add_questions':[],'resolve_questions':[],
'working_brief':{'format_version':2,'findings':findings,'uncertainties':uncertainties,
'next_question':question[:300],
'retired':[{'id':fid,'reason':'Explicit model retirement; full reason retained in bound LLM response.'} for fid in retired]}}}
def normalize(context,reply):
result=previous.normalize(legacy(context),wire(context,reply))
if previous.checkpoint(context) and reply['next_question'] and len(reply['next_question'])>300:
result['update']['working_brief']['next_question_details']=reply['next_question']
return result
def validate_reply(context,reply):
from jsonschema import Draft202012Validator
from viano_bug_analysis.llm.diagnostics import schema_feedback
error=next(Draft202012Validator(schema(context,strict=True)).iter_errors(reply),None)
if error:
if context.get('policy', {}).get('prompt_window_version') == 2:
from .investigation_contract import feedback
raise InvestigationError('INVESTIGATION_OUTPUT', feedback=feedback(error,reply))
from .tool_guidance import filter_feedback
raise InvestigationError('INVESTIGATION_OUTPUT',feedback=filter_feedback(previous.repair_feedback(schema_feedback(error,include_keyword=True))))
if previous.checkpoint(context):
from .investigation_compact import validate_tool_shape
validate_tool_shape(legacy(context),wire(context,reply))