Produce coding and human remediation handoffs for confirmed diagnoses

This commit is contained in:
Nico committed 2026-10-15 19:15:25 +02:00
1 parent c1f44e4e02
commit a318faf3e6
18 files changed
+2177 -323

No files matched your search

+10 -7
View File
@@ -27,6 +27,9 @@ class RequestAdapter:
'forbidden_changes':['Unrelated database changes.'],'do_not_fix_by':['Disabling the interaction.'],
'validation_requirements':[{'description':'The reported interaction reaches the expected state; other interactions remain intact.','evidence_refs':[eid]}],
'missing_information':[],'reason':'A narrow implementation scope is available.'}
if 'human_dev' in request['output_schema']['required']:
p.update(placeholders=[],human_dev={'status':'none','title':'Kein Human-Dev-Auftrag vorhanden',
'reason':'The coding assignment has sufficient context.','instructions':[],'expected_results':[]})
if self.change:self.change(p,c)
return ProviderResponse(p,'fixture-change-request',100,50)
@@ -97,25 +100,25 @@ def test_invalid_reply_has_no_partial_task_or_debit(diagnosed,change):
assert r.manifest['artifacts']['change_request'] is None and r.manifest['state']==State.BUILDING_DIAGNOSIS
assert BudgetManager(store).snapshot(*CASE).state==before
def test_missing_context_and_invalid_limit_go_to_review(diagnosed):
def test_missing_remediation_data_goes_to_human_handoff(diagnosed):
store,_=diagnosed
def insufficient(p,c):p.update(status='insufficient_context',affected_scope=[],validation_requirements=[],missing_information=['Obtain the actual activation definition.'])
def insufficient(p,c):p.update(status='none',affected_scope=[],validation_requirements=[],implementation_guidance=[],missing_information=['Obtain the actual activation definition.'],human_dev={'status':'assigned','title':'Collect remediation data','reason':'Placement information is absent.','instructions':['Collect approved placement information.'],'expected_results':['Provide the collected information to the implementer.']})
adapter=RequestAdapter(insufficient);builder=ChangeRequestBuilder(store,gateway(adapter))
result=build(builder);assert result.status=='INCONCLUSIVE' and result.failure_ref
result=build(builder);assert result.status=='COMPLETED' and result.failure_ref is None
assert build(builder).reused and adapter.calls==1
exported=asyncio.run(OutputValidator(store,gateway(adapter)).publish(*CASE))
assert exported.status=='REVIEW' and exported.exported.parent==store.workspace.review
assert exported.status=='OUTPUT_READY' and exported.exported.parent==store.workspace.outbox
with PackageReader(exported.package) as r:
assert r.manifest['state']==State.FAILED_TO_ANALYZE and r.manifest['artifacts']['change_request'] is None
assert r.manifest['state']==State.OUTPUT_READY and r.manifest['artifacts']['change_request']
assert json.loads(r.read_bytes(r.manifest['artifacts']['diagnosis']))['diagnosis']['status']=='confirmed'
def test_invalid_limit_fallback_and_replay(diagnosed):
store,_=diagnosed;adapter=RequestAdapter(lambda p,c:p.update(affected_scope=[]))
builder=ChangeRequestBuilder(store,gateway(adapter),ChangeRequestConfig(2))
assert build(builder).status=='REJECTED'
result=build(builder);assert result.status=='INCONCLUSIVE' and adapter.calls==2
result=build(builder);assert result.status=='COMPLETED' and adapter.calls==2
assert build(ChangeRequestBuilder(store,gateway(adapter))).reused and adapter.calls==2
assert asyncio.run(OutputValidator(store,gateway(adapter)).publish(*CASE)).status=='REVIEW'
assert asyncio.run(OutputValidator(store,gateway(adapter)).publish(*CASE)).status=='OUTPUT_READY'
def test_inconclusive_diagnosis_never_creates_coding_request(tool_case):
store=tool_case[0];asyncio.run(Investigator(store,gateway(LoopAdapter())).run(*CASE,max_steps=20))
+164
View File
@@ -0,0 +1,164 @@
import asyncio
import copy
import json
import pytest
from viano_bug_analysis.analysis import ChangeRequestBuilder, OutputValidator, OutputError
from viano_bug_analysis.analysis.change_request_contracts import validate_request, make_document, render_human_request, ChangeRequestError
from viano_bug_analysis.budget import BudgetManager
from viano_bug_analysis.evidence.store import digest
from viano_bug_analysis.lifecycle import State, CheckpointStore, Workspace
from viano_bug_analysis.lifecycle.storage import json_bytes
from viano_bug_analysis.package import PackageReader, PackageWriter, PackageValidator
from test_case_builder import case_setup
from test_case_interpreter import gateway
from test_investigation_tools import tool_case
from test_change_request_builder import diagnosed, RequestAdapter, build
from test_investigator import CASE
def human():
return {'status':'assigned','title':'Collect approved placement data',
'reason':'The cause is established but remediation data is missing.',
'instructions':['Record approved positions and complete the template externally.'],
'expected_results':['Placement data and a completed remediation template for the next instance.']}
def template(payload, context):
payload.update(status='template',missing_information=['Approved placement coordinate.'],
placeholders=[{'token':'{{POSITION_X}}','description':'Approved horizontal coordinate.'}],
implementation_guidance=['Create a template using {{POSITION_X}}; do not guess the coordinate.'],
human_dev=human())
def no_coding(payload, context):
payload.update(status='none',affected_scope=[],validation_requirements=[],implementation_guidance=[],
required_change='Kein Codingauftrag vorhanden',implementation_intent='No coding assignment is needed.',
reason='Remediation is an external human task.',human_dev=human())
def test_template_and_human_data_task_are_completed_handoffs(diagnosed, tmp_path):
store, original=diagnosed
before=BudgetManager(store).snapshot(*CASE).state
adapter=RequestAdapter(template);g=gateway(adapter)
result=build(ChangeRequestBuilder(store,g))
assert result.status=='COMPLETED' and result.failure_ref is None
with PackageReader(original) as old, PackageReader(result.package) as reader:
coding=json.loads(reader.read_bytes(reader.manifest['artifacts']['change_request']))
assert coding['assignment']=='assigned' and coding['request']['status']=='template'
assert coding['human_dev_assigned'] is True and 'human_dev' not in coding['request']
assert coding['request']['placeholders'][0]['token']=='{{POSITION_X}}'
text=reader.read_bytes(reader.manifest['artifacts']['human_dev_request']).decode()
assert 'Auftrag vorhanden' in text and 'complete the template externally' in text
assert not reader.manifest['artifacts']['failure_report']
for name in old.files:
if name!='manifest.json':assert reader.read_bytes(name)==old.read_bytes(name)
# No readiness/dependency or human-completion gate: the unresolved template
# and data task are exported immediately, with unchanged research budgets.
exported=asyncio.run(OutputValidator(store,g).publish(*CASE))
assert exported.status=='OUTPUT_READY' and exported.exported.parent==store.workspace.outbox
assert BudgetManager(store).snapshot(*CASE).state==before
assert PackageValidator().validate(exported.exported).valid
portable=CheckpointStore(Workspace(tmp_path/'recipient'));portable.intake(exported.exported)
assert asyncio.run(OutputValidator(portable,g).publish(*CASE)).status=='OUTPUT_READY'
assert adapter.calls==1
store.advance(*CASE,State.HANDED_OFF,step_id='recipient-ack')
@pytest.mark.parametrize('human_assigned',[False,True])
def test_no_coding_assignment_still_exports_both_documents(diagnosed,human_assigned):
store,_=diagnosed
def change(p,c):
no_coding(p,c)
if not human_assigned:
p['human_dev']={'status':'none','title':'Kein Human-Dev-Auftrag vorhanden',
'reason':'No additional human work is needed.','instructions':[],'expected_results':[]}
adapter=RequestAdapter(change);g=gateway(adapter)
assert build(ChangeRequestBuilder(store,g)).status=='COMPLETED'
result=asyncio.run(OutputValidator(store,g).publish(*CASE))
assert result.status=='OUTPUT_READY'
with PackageReader(result.package) as reader:
doc=json.loads(reader.read_bytes(reader.manifest['artifacts']['change_request']))
assert doc['assignment']=='none' and doc['human_dev_assigned']==human_assigned
assert reader.read_bytes(reader.manifest['artifacts']['human_dev_request'])
assert reader.manifest['routing']['destination']=='outbox'
def test_missing_human_document_pointer_prevents_export(diagnosed,tmp_path):
store,_=diagnosed;g=gateway(RequestAdapter())
result=build(ChangeRequestBuilder(store,g));bad=tmp_path/'pointer.zip'
PackageWriter().write(result.package,bad,manifest_updates={'artifacts':{'human_dev_request':None}})
store.intake(bad)
with pytest.raises(OutputError):asyncio.run(OutputValidator(store,g).publish(*CASE))
assert not list(store.workspace.outbox.glob('*.zip'))
@pytest.mark.parametrize('legacy_insufficient',[False,True])
def test_legacy_pending_request_keeps_its_prompt_and_portable_replay(diagnosed,legacy_insufficient):
def legacy_change(p,c):
if legacy_insufficient:
p.update(status='insufficient_context',affected_scope=[],validation_requirements=[],
missing_information=['Approved placement data is missing.'])
store,_=diagnosed;adapter=RequestAdapter(legacy_change);g=gateway(adapter);builder=ChangeRequestBuilder(store,g)
with PackageReader(store.latest(*CASE)) as reader:context=builder.context(reader,3)
ref='analysis/change-requests/0001/context.json';raw=json_bytes(context)
store.advance(*CASE,State.BUILDING_DIAGNOSIS,step_id='change-request:v1:1:start',
details={'operation':'change-request-start','context_ref':ref,'context_sha256':digest(context)},
additions={ref:raw},artifacts={'change_request_context':ref})
result=build(builder)
assert result.status=='COMPLETED' and build(builder).reused and adapter.calls==1
with PackageReader(result.package) as reader:
doc=json.loads(reader.read_bytes(result.change_request_ref))
assert doc['format_version']==2 and reader.read_bytes(ref)==raw
assert reader.manifest['artifacts']['human_dev_request']
assert doc['assignment']==('none' if legacy_insufficient else 'assigned')
assert doc['human_dev_assigned']==legacy_insufficient
assert reader.manifest['state']==State.BUILDING_DIAGNOSIS
assert asyncio.run(OutputValidator(store,g).publish(*CASE)).status=='OUTPUT_READY'
assert adapter.calls==1
def minimal():
context={'handoff_protocol':2,'local_context':{'code.cpp':{'kind':'source'}},
'evidence':{'E000001':{'record':{'content_ref':'capture.json'},'content':{'items':[{'source_ref':'code.cpp'}]}}},
'diagnosis':{'diagnosis':{'root_cause':{'evidence_refs':['E000001']}}}}
payload={'status':'ready','problem_summary':'Observed defect','affected_scope':[{'kind':'source','source_ref':'code.cpp','evidence_refs':['E000001']}],
'required_change':'Correct the defect','implementation_intent':'Bounded correction',
'implementation_guidance':['Use the verified context'],'constraints':[],'preserve':[],'forbidden_changes':[],
'do_not_fix_by':[],'validation_requirements':[{'description':'Expected behavior restored','evidence_refs':['E000001']}],
'missing_information':[],'reason':'Sufficient coding context','placeholders':[],
'human_dev':{'status':'none','title':'No human work','reason':'No human task is needed','instructions':[],'expected_results':[]}}
return payload,context
@pytest.mark.parametrize('change,code',[
(lambda p: p.update(status='template',missing_information=['Coordinate']), 'PLACEHOLDERS'),
(lambda p: p.update(status='none'), 'NO_TASK_CONTRADICTORY'),
(lambda p: p['human_dev'].update(status='assigned'), 'HUMAN_DEV_INCOMPLETE'),
(lambda p: p['human_dev'].update(instructions=['Unexpected hidden work']), 'HUMAN_DEV_CONTRADICTORY'),
(lambda p: p.update(placeholders=[{'token':'{{X}}','description':'Value'}]), 'PLACEHOLDERS'),
])
def test_inconsistent_handoff_rejected(change,code):
payload,context=minimal();change(payload)
with pytest.raises(ChangeRequestError,match=code):validate_request(payload,context)
def test_coding_fields_do_not_contain_human_task_body():
payload,context=minimal();payload['human_dev']=human()
context.update(case_id='case',package_id='package',analysis_revision=1,diagnosis_ref='diagnosis.json',diagnosis_sha256='0'*64,
interpretation={'observed_behavior':[],'expected_behavior':[]})
context['local_context']['code.cpp'].update(source_ref='code.cpp',sha256='0'*64,size=1)
doc=make_document(context,payload,'context.json','call.json')
assert doc['human_dev_assigned']
assert 'Record approved positions' not in json.dumps(doc)
assert 'Record approved positions' in render_human_request(context,payload)
def test_verified_project_capture_can_ground_a_database_template():
payload,context=minimal()
template(payload,context)
payload['affected_scope']=[{'kind':'database','source_ref':'capture.json','evidence_refs':['E000001']}]
context['local_context']={'capture.json':{'kind':'database'}}
context['evidence']['E000001']['content']['items']=[{'query':{'kind':'database.rows'},'result':{'items':[{'row':{'id':1}}]}}]
validate_request(payload,context)
+1 -1
View File
@@ -29,7 +29,7 @@ class PipelineAdapter:
self.calls += 1
phase = {"case_interpretation_v1": "case", "hypotheses_v1": "hypotheses", "hypotheses_v2": "hypotheses",
"investigation_v1": "loop", "investigation_v2": "loop", "investigation_v3": "loop", "diagnosis_v1": "diagnosis", "diagnosis_v2": "diagnosis",
"change_request_v1": "request"}[request["prompt_version"]]
"change_request_v1": "request", "change_request_v2": "request"}[request["prompt_version"]]
adapter = self.adapters[phase]
return await adapter.generate(config, request, **kwargs)