Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a087b0-7b1e-7531-8ef3-6525ebaf7ee7
139 lines
10 KiB
Python
Executable file
139 lines
10 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Execute the FIAM Declared-vs-Observed Lens with estate coverage findings."""
|
|
from __future__ import annotations
|
|
import argparse
|
|
from pathlib import Path
|
|
import yaml
|
|
|
|
RECON_EXT='org.fiam.reconciliation_candidates'
|
|
|
|
def evidence_records(a):
|
|
e=a.get('evidence');return [e] if isinstance(e,dict) else [x for x in (e or []) if isinstance(x,dict)]
|
|
def has_mode(a,modes):return any(e.get('mode') in modes for e in evidence_records(a))
|
|
def confidence(a):return max([float(e.get('confidence',0)) for e in evidence_records(a)] or [0])
|
|
def source_summary(a):
|
|
if not a:return []
|
|
out=[]
|
|
for e in evidence_records(a):
|
|
s=e.get('source');out.append({'mode':e.get('mode'),'type':s.get('type') if isinstance(s,dict) else None,'ref':s.get('ref') if isinstance(s,dict) else s,'locator':s.get('locator') if isinstance(s,dict) else None,'confidence':e.get('confidence')})
|
|
return out
|
|
|
|
def index(doc):
|
|
obj={};kinds={};op_contract={}
|
|
for sec,kind in [('participants','participant'),('nodes','system-node'),('ports','port'),('contracts','interface-contract'),('endpoints','endpoint'),('bindings','binding'),('channels','channel'),('information_resources','information-resource'),('information_shapes','information-shape'),('interactions','interaction'),('interaction_flows','interaction-flow'),('artifacts','artifact'),('zones','zone'),('security','security-profile')]:
|
|
for x in doc.get(sec,[]) or []:
|
|
if x.get('id'):obj[x['id']]=x;kinds[x['id']]=kind
|
|
if sec=='contracts':
|
|
for o in x.get('operations',[]) or []:
|
|
if o.get('id'):obj[o['id']]=o;kinds[o['id']]='operation';op_contract[o['id']]=x['id']
|
|
return obj,kinds,op_contract
|
|
|
|
def ancestors(term,terms):
|
|
out=set();stack=list((terms.get(term) or {}).get('broader') or [])
|
|
while stack:
|
|
x=stack.pop()
|
|
if x in out:continue
|
|
out.add(x);stack.extend((terms.get(x) or {}).get('broader') or [])
|
|
return out
|
|
|
|
def depth(term,terms,memo=None):
|
|
memo=memo or {}
|
|
if term in memo:return memo[term]
|
|
br=(terms.get(term) or {}).get('broader') or [];memo[term]=0 if not br else 1+max(depth(x,terms,memo) for x in br);return memo[term]
|
|
|
|
def choose(assertions,strategy,terms):
|
|
if not assertions:return None
|
|
if strategy=='canon-hierarchy':
|
|
vals=[a for a in assertions if isinstance(a.get('value'),str) and a.get('value') in terms]
|
|
if vals:return max(vals,key=lambda a:(depth(a['value'],terms),confidence(a)))
|
|
return max(assertions,key=confidence)
|
|
|
|
def compare_values(dv,ov,strategy,terms):
|
|
if dv is None and ov is None:return 'unknown'
|
|
if dv is None:return 'observed-only'
|
|
if ov is None:return 'declared-only'
|
|
if dv==ov:return 'exact'
|
|
if strategy=='canon-hierarchy' and isinstance(dv,str) and isinstance(ov,str) and dv in terms and ov in terms:
|
|
oa=ancestors(ov,terms);da=ancestors(dv,terms)
|
|
if dv in oa:return 'compatible-refinement'
|
|
if ov in da:return 'observed-less-specific'
|
|
return 'conflict'
|
|
|
|
def owner_for(entity,obj,kinds,op_contract):
|
|
k=kinds.get(entity);bid=None
|
|
if k=='binding':bid=entity
|
|
elif k=='operation':
|
|
cid=op_contract.get(entity);bid=next((x for x,b in obj.items() if kinds.get(x)=='binding' and b.get('contract')==cid and b.get('provider_port')),None)
|
|
elif k=='endpoint':bid=next((x for x,b in obj.items() if kinds.get(x)=='binding' and entity in (b.get('endpoints') or []) and b.get('provider_port')),None)
|
|
elif k in {'channel','information-resource'}:
|
|
nid=(obj.get(entity) or {}).get('owner');return {'node_ref':nid,'name':(obj.get(nid) or {}).get('name')} if nid else None
|
|
if not bid:return None
|
|
p=obj.get((obj.get(bid) or {}).get('provider_port'),{});nid=p.get('owner');return {'node_ref':nid,'name':(obj.get(nid) or {}).get('name')} if nid else None
|
|
|
|
def entity_mode(ids,assertions,mode):
|
|
return {i for i in ids if any(a.get('subject')==i and has_mode(a,{mode}) for a in assertions)}
|
|
|
|
def coverage(doc,obj,kinds,op_contract,assertions):
|
|
# Entity coverage is intentionally relation-based and keeps ambiguous near-matches visible.
|
|
declared_mode='fiam.evidence.declared';observed_mode='fiam.evidence.runtime-observed'
|
|
opids=[i for i,k in kinds.items() if k=='operation'];chids=[i for i,k in kinds.items() if k=='channel']
|
|
declared_ops=entity_mode(opids,assertions,declared_mode);observed_ops=entity_mode(opids,assertions,observed_mode)
|
|
declared_ch=entity_mode(chids,assertions,declared_mode);observed_ch=entity_mode(chids,assertions,observed_mode)
|
|
corr={}
|
|
for a in assertions:
|
|
if a.get('assertion_kind')=='relation' and a.get('polarity')=='asserted' and a.get('predicate')=='fiam.relation.corresponds-to':corr.setdefault(a.get('subject'),set()).add(a.get('object_ref'))
|
|
candidates={x.get('observed_ref'):x for x in (doc.get('extensions',{}) or {}).get(RECON_EXT,[]) or []}
|
|
def classify(declared,observed,kind):
|
|
seen_declared={o for s in observed for o in corr.get(s,set()) if o in declared};rows=[]
|
|
for d in sorted(declared):
|
|
rows.append({'entity_kind':kind,'classification':'observed' if d in seen_declared else 'declared-only','declared_ref':d,'owner':owner_for(d,obj,kinds,op_contract)})
|
|
for o in sorted(observed):
|
|
if corr.get(o,set()) & declared:continue
|
|
cand=candidates.get(o) or {};status=cand.get('status') or 'unmatched';rows.append({'entity_kind':kind,'classification':'ambiguous' if status in {'ambiguous','near-match'} else 'observed-only','observed_ref':o,'candidate_status':status,'candidates':cand.get('candidates',[])})
|
|
return rows
|
|
rows=classify(declared_ops,observed_ops,'operation')+classify(declared_ch,observed_ch,'channel')
|
|
summary={}
|
|
for r in rows:summary[r['classification']]=summary.get(r['classification'],0)+1
|
|
return {'summary':dict(sorted(summary.items())),'findings':rows}
|
|
|
|
def execute(doc,canon,lens):
|
|
obj,kinds,op_contract=index(doc);terms={t['id']:t for t in canon.get('terms',[])};assertions=doc.get('assertions',[]) or [];declared=set(lens.get('declared_evidence_modes') or ['fiam.evidence.declared']);observed=set(lens.get('observed_evidence_modes') or ['fiam.evidence.runtime-observed'])
|
|
pairs=[]
|
|
for a in assertions:
|
|
if a.get('assertion_kind')=='relation' and a.get('polarity')=='asserted' and a.get('predicate')==lens['pair_relation']:
|
|
s,o=a.get('subject'),a.get('object_ref')
|
|
if s in kinds and o in kinds and kinds[s]==kinds[o]:pairs.append((s,o,a))
|
|
findings=[];seen=set()
|
|
for observed_ref,declared_ref,pa in pairs:
|
|
k=kinds.get(observed_ref)
|
|
for comp in lens['comparisons']:
|
|
if comp.get('subject_kinds') and k not in comp['subject_kinds']:continue
|
|
pred=comp['predicate'];strategy=comp['strategy'];da=[a for a in assertions if a.get('assertion_kind')=='property' and a.get('polarity')=='asserted' and a.get('subject')==declared_ref and a.get('predicate')==pred and has_mode(a,declared)];oa=[a for a in assertions if a.get('assertion_kind')=='property' and a.get('polarity')=='asserted' and a.get('subject')==observed_ref and a.get('predicate')==pred and has_mode(a,observed)]
|
|
if not da and not oa:continue
|
|
d=choose(da,strategy,terms);o=choose(oa,strategy,terms);cls=compare_values(d.get('value') if d else None,o.get('value') if o else None,strategy,terms);key=(declared_ref,observed_ref,pred,cls)
|
|
if key in seen:continue
|
|
seen.add(key);findings.append({'declared_ref':declared_ref,'observed_ref':observed_ref,'entity_kind':k,'predicate':pred,'classification':cls,'declared_value':d.get('value') if d else None,'observed_value':o.get('value') if o else None,'owner':owner_for(declared_ref,obj,kinds,op_contract),'pair_evidence':source_summary(pa),'declared_evidence':source_summary(d),'observed_evidence':source_summary(o)})
|
|
counts={}
|
|
for f in findings:counts[f['classification']]=counts.get(f['classification'],0)+1
|
|
cov=coverage(doc,obj,kinds,op_contract,assertions)
|
|
return {'lens':lens['id'],'lens_version':lens.get('version'),'model':doc.get('instance',{}).get('id'),'summary':{'pairs':len(pairs),'property_findings':len(findings),'classifications':dict(sorted(counts.items())),'coverage':cov['summary']},'findings':findings,'coverage_findings':cov['findings'],'reconciliation_candidates':(doc.get('extensions',{}) or {}).get(RECON_EXT,[]) or []}
|
|
|
|
def markdown(r):
|
|
lines=[f"# {r['lens']} result",'',f"Model: `{r.get('model')}`",'', '## Property comparison','']
|
|
for k,v in r['summary']['classifications'].items():lines.append(f'- **{k}:** {v}')
|
|
lines += ['','## Coverage','']+[f'- **{k}:** {v}' for k,v in r['summary']['coverage'].items()]
|
|
lines += ['','## Property findings','']
|
|
for f in r['findings']:
|
|
lines += [f"### {f['classification']} — {f['predicate']}",f"- Declared: `{f['declared_ref']}` = `{f.get('declared_value')}`",f"- Observed: `{f['observed_ref']}` = `{f.get('observed_value')}`"]
|
|
if f.get('owner'):lines.append(f"- Architectural owner: `{f['owner'].get('node_ref')}` ({f['owner'].get('name')})")
|
|
lines.append('')
|
|
lines += ['## Coverage findings','']
|
|
for f in r['coverage_findings']:
|
|
ref=f.get('declared_ref') or f.get('observed_ref');lines.append(f"- **{f['classification']}** {f['entity_kind']}: `{ref}`"+(f" ({f.get('candidate_status')})" if f.get('candidate_status') else ''))
|
|
return '\n'.join(lines)+'\n'
|
|
|
|
def main():
|
|
root=Path(__file__).resolve().parents[1];ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('model',type=Path);ap.add_argument('--canon',type=Path,default=root/'canon/FIAM-Canon-v0.2.yaml');ap.add_argument('--lens',type=Path,default=root/'lenses/declared-vs-observed.yaml');ap.add_argument('-o','--output',type=Path,required=True);ap.add_argument('--markdown',type=Path);args=ap.parse_args();doc=yaml.safe_load(args.model.read_text());canon=yaml.safe_load(args.canon.read_text());lens=yaml.safe_load(args.lens.read_text());r=execute(doc,canon,lens);args.output.parent.mkdir(parents=True,exist_ok=True);args.output.write_text(yaml.safe_dump(r,sort_keys=False,allow_unicode=True,width=120));
|
|
if args.markdown:args.markdown.write_text(markdown(r))
|
|
print(yaml.safe_dump(r['summary'],sort_keys=False).strip())
|
|
if __name__=='__main__':main()
|