47 lines
3.3 KiB
Python
47 lines
3.3 KiB
Python
"""Observable trajectory details and immutable run comparisons (AC-77)."""
|
|
from services import assistant_store as store
|
|
|
|
|
|
def step_details(run):
|
|
components=run.get('components') or {}
|
|
methods={c['id']:c for c in components.get('skills',[])+components.get('rules',[])}
|
|
evidence={e['id']:e for e in run.get('evidence',[]) if e.get('id')}
|
|
details={}
|
|
for raw in run.get('steps',[]):
|
|
key=raw['step_id']; component=methods.get(key,{})
|
|
checks=[{k:c.get(k) for k in ('code','title','result','fields','evidence_ids','suggested_action','basis_ids')}
|
|
for c in raw.get('findings',[])]
|
|
ids={e for c in checks for e in c.get('evidence_ids') or []}
|
|
details[key]={'name':raw.get('name',key), 'action':component.get('instructions') or component.get('description') or raw.get('name',key),
|
|
'declared_inputs':component.get('inputs',[]), 'conditions':component.get('conditions',[]),
|
|
'exceptions':component.get('exception_note') or component.get('exceptions',[]), 'checks':checks,
|
|
'evidence':[{k:evidence[e].get(k) for k in ('id','field','value','source_id','locator','document_id')} for e in ids if e in evidence],
|
|
'missing_inputs':raw.get('missing_inputs',[])}
|
|
return details
|
|
|
|
|
|
def compare(db,project_id,left_id,right_id):
|
|
left=store.get(db,'run',left_id);right=store.get(db,'run',right_id)
|
|
if left.get('project_id')!=project_id or right.get('project_id')!=project_id:
|
|
raise KeyError('运行不存在或不属于该项目')
|
|
def summary(run):
|
|
snapshot=store.get(db,'input_snapshot',run['input_snapshot_id'])
|
|
components=run.get('components') or {}
|
|
return {'id':run['id'],'status':run['status'],'created_at':run['created_at'],
|
|
'snapshot_id':run['input_snapshot_id'],'instruction':run.get('requirement',''),
|
|
'documents':[{k:d.get(k) for k in ('id','name','version','content_hash')} for d in snapshot.get('documents',[])],
|
|
'plan':{k:(components.get('plan') or {}).get(k) for k in ('id','version')},
|
|
'components':[{'id':c['id'],'version':c.get('version'),'content_hash':c.get('content_hash')} for c in components.get('skills',[])+components.get('rules',[])],
|
|
'model':{k:(run.get('model_config') or {}).get(k) for k in ('available','name')},
|
|
'checks':[{k:c.get(k) for k in ('code','title','result','suggested_action','evidence_ids')} for c in run.get('checks',[])]}
|
|
a,b=summary(left),summary(right)
|
|
old={c['code']:c for c in a['checks']};new={c['code']:c for c in b['checks']}
|
|
open_results=('issue','unknown','error')
|
|
delta={'resolved':[new[k] for k in new if k in old and old[k]['result'] in open_results and new[k]['result']=='pass'],
|
|
'new':[new[k] for k in new if k not in old and new[k]['result'] in open_results],
|
|
'remaining':[new[k] for k in new if k in old and new[k]['result'] in open_results],
|
|
'unexecuted':[old[k] for k in old if k not in new]}
|
|
config_changed=a['plan']!=b['plan'] or a['components']!=b['components'] or a['model']!=b['model'] or a['instruction']!=b['instruction']
|
|
return {'left':a,'right':b,'delta':delta,'configuration_changed':config_changed,'read_only':True,
|
|
'note':'两次配置、模型或指令有变化,差异不能全部归因于补件。' if config_changed else '仅展示两次冻结结果差异;相关变化不能单独证明补件因果效应。'}
|