from __future__ import annotations import hashlib,json from .affordance_engine import mine_rules,RULE_META,GENERIC_TYPES from .claim_ir import claim_key,summary def _hash(x):return hashlib.sha256(json.dumps(x,sort_keys=True,ensure_ascii=False,separators=(',',':')).encode()).hexdigest() class IncrementalMiner: """Type-indexed agenda: only rules touched by changed interfaces are recomputed; downstream claim rules propagate once.""" def __init__(self): self.type_sig={};self.claim_hash={};self.claims={};self.rule_claims={k:[] for k in RULE_META};self.recompute_count=0;self.rule_evaluations=0 def _type_sigs(self,facts): grouped={} for t in GENERIC_TYPES: rows=[{k:f.get(k) for k in ('id','type','payload','assumptions','domain','provenance')} for f in facts if f.get('type')==t] if rows:grouped[t]=_hash(sorted(rows,key=lambda x:str(x.get('id')))) return grouped def update(self,facts:list[dict])->dict: sig=self._type_sigs(facts);changed={t for t in set(sig)|set(self.type_sig) if sig.get(t)!=self.type_sig.get(t)} if not changed:return {'changed':False,'delta':[],'recompute_count':self.recompute_count,'rule_evaluations':self.rule_evaluations} self.type_sig=sig;self.recompute_count+=1 active={name for name,(ft,_) in RULE_META.items() if ft & changed} # claim-dependent rules are activated when their upstream producer is active. produced_kinds=set() for name in list(active): if name=='relation_extent':produced_kinds.add('relation_extent') active|={name for name,(_,ck) in RULE_META.items() if ck & produced_kinds} prior=[c for rows in self.rule_claims.values() for c in rows] out=mine_rules(facts,prior,active);self.rule_evaluations+=len(active) for name in active:self.rule_claims[name]=out.get(name,[]) # Re-run downstream inverse-map once with refreshed extents if needed. if 'relation_extent' in active and 'inverse_map' in active: prior=[c for rows in self.rule_claims.values() for c in rows] self.rule_claims['inverse_map']=mine_rules(facts,prior,{'inverse_map'}).get('inverse_map',[]);self.rule_evaluations+=1 claims=[c for rows in self.rule_claims.values() for c in rows] current={};delta=[] for c in claims: key=claim_key(c);h=_hash(c);current[key]=c if self.claim_hash.get(key)!=h:delta.append({'key':key,'claim':c,'ir':summary(c)}) self.claim_hash={k:_hash(v) for k,v in current.items()};self.claims=current return {'changed':True,'delta':delta,'recompute_count':self.recompute_count,'rule_evaluations':self.rule_evaluations} def full_results(self):return list(self.claims.values())