"""Offline opportunity evidence study. Writes only under this report directory.

Evidence labels refer to historical Native executable-control inventory, NOT the
completeness of a retained GUI tree or successful execution of one selected input.
No resolver, validator, Native binary, device or provider is called.
"""
import bisect
import copy
from evidence_common import *

STUDY4=ROOT.parent/'region-dominator-backtrace-20261107'
STUDY2=ROOT.parent/'region-entry-source-investigation-20261107'
FOCUS={
 'odkcollect':{'download','start-new-form','drafts','settings'},
 'activitydiary':{'add-activity','edit-existing-activity','settings','manage-activities'},
 'omninotes':{'settings','search-notes','attachment','set-reminder'},
 'scarletnotes':{'create-note-empty-state','settings','search-notes'},
 'wikipedia':{'global-search','article-gallery','reading-theme','find-in-article','edit-article'},
}
CORE={'odkcollect.download','activitydiary.add-activity','activitydiary.edit-existing-activity',
      'omninotes.settings','omninotes.search-notes','scarletnotes.create-note-empty-state','wikipedia.global-search'}
WINDOWS=(2,5,10,20)

def safe_table(name,rows):
    rows=list(rows)
    if rows:table(name,rows)
    else:write('analysis/'+name+'.json',[])

def load_run(meta,prior):
    assert meta['status']=='COMPLETED_VALID' and meta['condition']=='pure'
    base=Path(meta['graph_path']).parent; raw=lines(base/'jev-events.jsonl')
    assert READ_FILES[str(base/'jev-events.jsonl')]==meta['events_sha256']
    assert read(base/'jev-evidence-complete.json')['healthy']
    finished=[e for e in raw if e['event']=='RUN_FINISHED']
    assert len(finished)==1 and not finished[0]['region_enabled']
    native=[(n,e) for n,e in enumerate(raw,1) if e['event']=='NATIVE_TRANSITION']
    idx={e['transition_id']:(i,n,e) for i,(n,e) in enumerate(native)}
    es=[copy.deepcopy(e) for e in prior if (e['app'],e['rep'])==(meta['app'],meta['rep'])]
    assert len(es)==sum(bool(e['input_confirmed']) for _,e in native)
    guis=[];gi=defaultdict(list)
    for p in sorted((base/'jev-audit/gui').glob('*.json')):
        g=read(p);g['evidence_path']=str(p);guis.append(g)
        gi[(g['gui_id'],g['gui_timestamp'])].append(g)
    sels=[dict(e,event_line=n) for n,e in enumerate(raw,1) if e['event']=='SELECTION_DECISION']
    si=defaultdict(list)
    for s in sels:si[(s['step'],s['abstraction_version'],s['state_id'],s['model_action_id'])].append(s)
    run=dict(id=meta['app']+'-'+meta['rep'],app=meta['app'],rep=meta['rep'],base=str(base),meta=meta,
             events=es,byid={e['transition_id']:e for e in es},selections=sels,guis=guis,gui_index=gi,
             raw_event_counts=dict(Counter(e['event'] for e in raw)),raw_transition_count=len(native))
    # Validate source receipts before deriving target Activity from the next source.
    for e in es:
        for f in ['resolved_target_activity','source_gui','target_gui','source_contexts','target_contexts',
                  'row_labels','source_screenshot','target_screenshot','a_regions']:
            e.pop(f,None)
        i,n,r=idx[e['transition_id']];assert e['event_line']==n and r['input_confirmed']
        inpdoc=read(e['input_path']);inp=inpdoc.get('input') or {};rec=read(e['receipt_path'])
        assert (e['state'],e['target_state'],e['model_action'],e['version'])==(r['state_id'],r['target_state'],r['model_action_id'],r['abstraction_version'])
        assert (e['source_gui_id'],e['source_gui_timestamp'])==(inp['source_gui_id'],rec['source_gui_timestamp'])
        e.update(input_ns=inp.get('monotonic_ns',inpdoc['monotonic_ns']),input_step=inp['step'],
                 input_version=inp['abstraction_version'],input_model_action=inp.get('concrete',{}).get('model_action_id'),
                 source_activity=rec['source_activity'],native_transition_order=i,raw_event=r,
                 wall_time_ms=r['wall_time_ms'],planned_input_text=inp.get('planned_input_text'))
        e['latest_selection']=next((s for s in reversed(si[(e['input_step'],e['input_version'],e['state'],e['input_model_action'])])
                                    if s['monotonic_ns']<=e['input_ns']),None)
        gs=gi[(e['source_gui_id'],e['source_gui_timestamp'])]
        e['retained_source_gui_paths']=[g['evidence_path'] for g in gs]
        e['source_gui_known_by_input']=[g['evidence_path'] for g in gs if g['monotonic_ns']<=e['input_ns']]
        e['source_key']=key(e['input_version'],e['state']);e['target_key']=key(e['version'],e['target_state'])
        e['concrete_key']=key(e['input_version'],e['state'],e['input_model_action'],e['identity_digest']) if e['identity_digest'] else None
        e['activity_concrete_key']=key(e['source_activity'],e['identity_digest']) if e['identity_digest'] else None
        e['model_transition_key']=key(e['version'],e['state'],e['model_action'],e['target_state'])
        e['ID_only_model_transition_key']=key(e['state'],e['model_action'],e['target_state'])
        e['concrete_transition_key']=key(e['input_version'],e['state'],e['input_model_action'],e['identity_digest'] or e['action_type'],e['version'],e['target_state'])
        e['action_class']=action_class(e);e['action_label']=label(e)
        e['operation_family_key']=key(e['source_activity'],e['action_type'],e['resource_id'] or e['class'],e['action_class'])
        e['inventory_level']='PARTIAL' if e['identity_digest'] or gs else 'INFERRED' if e['latest_selection'] else 'MISSING'
        e['exact_remaining_executable_concrete']=None
    for e in es:
        r=e['raw_event'];e['target_activity_evidence']=[]
        for g in gi[(e['target_gui_id'],e['target_gui_timestamp'])]:
            e['target_activity_evidence'].append(dict(activity=g['activity'],available_ns=max(r['monotonic_ns'],g['monotonic_ns']),
                provenance='same_GUI_retained_tree',path=g['evidence_path']))
        j=e['order']+1
        if j<len(es) and not continuity(e,es[j]):
            nxt=es[j]
            e['target_activity_evidence'].append(dict(activity=nxt['source_activity'],available_ns=nxt['monotonic_ns'],
                provenance='next_continuous_confirmed_source_receipt',transition_id=nxt['transition_id']))
        e['target_activity']=target_at(e,10**30)
    # Ordered global novelty; version-scoped facts are kept separate from bare-ID sensitivity.
    seenS=set();sets={f:set() for f in ['concrete_key','activity_concrete_key','model_transition_key','ID_only_model_transition_key','concrete_transition_key','operation_family_key']}
    flags=['first_local_concrete','first_activity_concrete','first_model_transition','first_ID_only_model_transition','first_concrete_transition','first_operation_family']
    ac=Counter();visits=Counter()
    for e in es:
        e['source_prior_confirmed_visits']=visits[e['source_key']];seenS.add(e['source_key'])
        e['first_target_state']=e['target_key'] not in seenS;seenS.add(e['target_key']);visits[e['target_key']]+=1
        for f,flag in zip(sets,flags):
            e[flag]=bool(e[f] and e[f] not in sets[f])
            if e[f]:sets[f].add(e[f])
        e['prior_local_concrete_executions']=ac[e['concrete_key']] if e['concrete_key'] else None
        if e['concrete_key']:ac[e['concrete_key']]+=1
    return run

def action_class(e):
    rid=e['resource_id'].split('/')[-1]
    if rid in {'action_edit_done','action_edit_delete','quickFixButton1','quickFixButton2'}:return 'mode_object_result_sensitive'
    if e['class'].endswith('CheckBox'):return 'row_configuration_sensitive'
    if e['class'].endswith(('EditText','AutoCompleteTextView')):return 'input_value_sensitive'
    if rid in {'edit_activity_color','hexCode','okColorButton'}:return 'shared_GUI_color_operation_not_result_equivalence'
    if e['action_type']=='MODEL_BACK':return 'context_dependent_navigation'
    return 'operation_equivalence_NOT_asserted'

def phase(run,fn,e,cutoff,seed_activity):
    a=target_at(e,cutoff)
    if not a:return None
    act=short(a)
    if fn=='odkcollect.download':return act=='FormDownloadListActivity'
    if fn in {'activitydiary.add-activity','activitydiary.edit-existing-activity'}:return act=='EditActivity'
    if fn=='omninotes.settings':return act=='SettingsActivity'
    if fn=='scarletnotes.create-note-empty-state':return act=='CreateNoteActivity'
    if fn=='wikipedia.global-search':
        if act=='SearchActivity':return True
        if act=='MainActivity':
            # Only search-tab landings already confirmed by this time; never use final State lists.
            land={x['target_state'] for x in run['events'] if x['monotonic_ns']<=cutoff and x['resource_id'].endswith('/nav_tab_search')}
            return e['target_state'] in land
        return False
    return a==seed_activity if seed_activity else None

def segment(run,entry,method='B',cutoff=10**30):
    assert entry['monotonic_ns']<=cutoff
    fn=entry['label']['function'];es=run['events'];seed=target_at(entry,cutoff)
    ids=[entry['transition_id']];stop='archive_end';complete=False;boundary=None
    mode='add' if fn=='activitydiary.add-activity' else 'edit' if fn=='activitydiary.edit-existing-activity' else fn
    p=phase(run,fn,entry,cutoff,seed)
    if method=='B' and p is not True:
        return dict(ids=ids,stop='entry_target_unknown' if p is None else 'entry_target_outside_scope',complete=False,boundary=None,mode=mode)
    limit=int(method[1:]) if method.startswith('A') else 10000
    last=entry
    for j in range(entry['order']+1,min(len(es),entry['order']+limit+1)):
        e=es[j]
        if e['monotonic_ns']>cutoff:stop='historical_cutoff';break
        gap=continuity(last,e)
        if gap:stop='continuity:'+','.join(gap);break
        if method=='B' and e['label']['category']=='Ⅰ':
            stop='next_independent_entry';complete=True;boundary=e['transition_id'];break
        ids.append(e['transition_id']);last=e
        if method=='B':
            rid=e['resource_id'].split('/')[-1]
            if rid=='quickFixButton1' and run['app']=='activitydiary':
                stop='object_mode_handoff';complete=True;boundary=e['transition_id'];break
            if fn=='wikipedia.global-search' and rid in {'more_languages','langCodeText','add_language_button','nav_tab_explore','nav_tab_reading_lists','nav_tab_edits','nav_tab_more','wikitext_button_link'}:
                stop='reviewed_function_handoff';complete=True;boundary=e['transition_id'];break
            p=phase(run,fn,e,cutoff,seed)
            if p is None:stop='target_activity_unknown';break
            if not p:stop='observed_activity_or_page_exit';complete=True;boundary=e['transition_id'];break
    else:
        if len(ids)==limit+1 and method.startswith('A'):stop='fixed_window_complete';complete=True
    return dict(ids=ids,stop=stop,complete=complete,boundary=boundary,mode=mode)

def candidates(g):
    """Retained GUI flag observations, NOT an executable Native action enumeration."""
    paths={};out=[]
    for n in g['nodes']:
        paths[n['id']]=(paths.get(n['parent'],'')+'-' if n['parent'] is not None else '')+str(n['index'])
        types=[]
        if n.get('clickable') or n.get('checkable'):types.append('MODEL_CLICK')
        if n.get('long_clickable'):types.append('MODEL_LONG_CLICK')
        if n.get('scrollable'):types.append('SCROLL_DIRECTION_UNKNOWN')
        for t in types:
            out.append(dict(node_id=n['id'],node_path=paths[n['id']],action_type=t,resource_id=n['resource_id'],
                text=n['native_text'],content_desc=n['native_content_desc'],class_name=n['class'],bounds=n['bounds'],
                enabled_flag=n.get('enabled'),checked_flag=n.get('checked'),
                status='GUI_FLAG_ENABLED_NATIVE_MEMBERSHIP_UNKNOWN' if n.get('enabled') else 'GUI_FLAG_DISABLED',
                executable_validity='UNKNOWN',model_action_id=None,concrete_identity=None))
    return out

def audit_inventory(run):
    es=run['events'];sels=run['selections'];guis=run['guis'];gi=run['gui_index']
    levels=Counter(e['inventory_level'] for e in es)
    dl=Counter('PARTIAL' if s.get('selected_resolved_node_count',0)>0 else 'INFERRED' for s in sels)
    arrivals=Counter('PARTIAL' if gi[(e['target_gui_id'],e['target_gui_timestamp'])] else 'INFERRED' for e in es)
    summary=dict(run=run['id'],app=run['app'],rep=run['rep'],confirmed_source_visits=len(es),confirmed_arrivals=len(es),
        decision_points=len(sels),retained_GUI_snapshots=len(guis),unique_retained_GUI_keys=len(gi)-sum(not v for v in gi.values()),
        same_GUI_source_visits=sum(bool(e['retained_source_gui_paths']) for e in es),
        same_GUI_source_fraction=sum(bool(e['retained_source_gui_paths']) for e in es)/len(es),
        GUI_logged_by_input=sum(bool(e['source_gui_known_by_input']) for e in es),
        same_GUI_arrivals=sum(bool(gi[(e['target_gui_id'],e['target_gui_timestamp'])]) for e in es),
        aligned_selection_sources=sum(e['latest_selection'] is not None for e in es),
        targeted_confirmed_inputs=sum(bool(e['concrete_key']) for e in es),
        multiple_resolved_member_decisions=sum(s.get('selected_resolved_node_count',0)>1 for s in sels),
        explicit_context_or_inventory_events=sum(v for k,v in run['raw_event_counts'].items() if 'CONTEXT_CAPTURED' in k or 'ACTION_INVENTORY' in k),
        source_EXACT=0,source_PARTIAL=levels['PARTIAL'],source_INFERRED=levels['INFERRED'],source_MISSING=levels['MISSING'],
        decision_EXACT=0,decision_PARTIAL=dl['PARTIAL'],decision_INFERRED=dl['INFERRED'],decision_MISSING=0,
        arrival_EXACT=0,arrival_PARTIAL=arrivals['PARTIAL'],arrival_INFERRED=arrivals['INFERRED'],arrival_MISSING=0,
        complete_ModelAction_ID_inventory_EXACT=sum(len((set(s['unvisited_model_action_ids'].split(','))-{''})|({s['model_action_id']} if s['model_action_id'] else set()))==s['model_action_count'] for s in sels),
        complete_resolved_member_inventory_EXACT=0,
        complete_valid_Concrete_inventory_EXACT=0,complete_strategy_eligible_Concrete_inventory_EXACT=0,
        exact_remaining_executable_concrete=None,action_coverage_fraction=None,
        archived_event_types=run['raw_event_counts'],input_observation_version_differences=sum(e['input_version']!=e['version'] for e in es),
        continuity_gaps=dict(Counter(r for a,b in zip(es,es[1:]) for r in continuity(a,b))))
    visits=[]
    for e in es:
        s=e['latest_selection']
        visits.append(dict(run=run['id'],transition_id=e['transition_id'],order=e['order'],input_ns=e['input_ns'],
            source_state=e['state'],input_version=e['input_version'],source_GUI=e['source_gui_id'],GUI_timestamp=e['source_gui_timestamp'],
            selected_concrete=e['identity_digest'],selected_ModelAction=e['input_model_action'],evidence_level=e['inventory_level'],
            same_GUI_retained=len(e['retained_source_gui_paths']),GUI_logged_by_input=len(e['source_gui_known_by_input']),
            selection_line=s['event_line'] if s else None,selection_model_count=s['model_action_count'] if s else None,
            selection_enabled_valid_count=s['enabled_valid_action_count'] if s else None,
            selection_enabled_valid_unvisited_count=s['enabled_valid_unvisited_action_count'] if s else None,
            selected_resolved_node_count=s['selected_resolved_node_count'] if s else None,
            current_valid_untried_Concrete=None,source_input_path=e['input_path'],receipt_path=e['receipt_path']))
    return summary,visits

def inventory_at(run,sources,cutoff):
    # Historical observations only; subtraction proves no confirmed attempt after snapshot,
    # NOT continued availability, and does not decrement native pre-input visit counters.
    snap={}
    for s in run['selections']:
        sk=key(s['abstraction_version'],s['state_id'])
        if s['monotonic_ns']<=cutoff and sk in sources:snap[sk]=s
    history=[e for e in run['events'] if e['monotonic_ns']<=cutoff]
    version=max((s['abstraction_version'] for s in run['selections'] if s['monotonic_ns']<=cutoff),default=None)
    rows=[]
    for sk,s in snap.items():
        ids=[m for m in s['unvisited_model_action_ids'].split(',') if m]
        executed={e['input_model_action'] for e in history if e['source_key']==sk and e['monotonic_ns']>s['monotonic_ns']}
        remaining=sorted(set(ids)-executed)
        rows.append(dict(source_key=sk,selection_line=s['event_line'],available_ns=s['monotonic_ns'],
            age_ns=cutoff-s['monotonic_ns'],model_action_count=s['model_action_count'],
            enabled_valid_count=s['enabled_valid_action_count'],enabled_valid_unvisited_at_observation=s['enabled_valid_unvisited_action_count'],
            unvisited_at_observation=ids,unconfirmed_since_observation=remaining,
            current_abstraction_version=json.loads(sk)[0]==version,
            scope='historical ModelAction opportunity; present validity/Concrete members UNKNOWN'))
    return dict(exact_remaining_executable_concrete=None,coverage_fraction=None,complete_inventory=False,
        evidence_level='PARTIAL' if rows else 'MISSING',current_version=version,snapshots=rows,
        historical_unconfirmed_ModelAction_ids=sum(len(r['unconfirmed_since_observation']) for r in rows),
        current_version_historical_unconfirmed_ModelAction_ids=sum(len(r['unconfirmed_since_observation']) for r in rows if r['current_abstraction_version']))

def checkpoints(run,fn,ocs):
    out=[];es=run['events']
    for frac in [.2,.4,.6,.8]:
        n=max(1,math.ceil(len(ocs)*frac));cutentry=run['byid'][ocs[n-1]['entry']];cut=cutentry['monotonic_ns']
        past=[segment(run,run['byid'][o['entry']],'B',cut) for o in ocs[:n]]
        ids={t for s in past for t in s['ids']};pe=[e for e in es if e['transition_id'] in ids]
        inv=inventory_at(run,{e['source_key'] for e in pe},cut)
        completed=[(o,s) for o,s in zip(ocs[:n],past) if s['complete']]
        recent=completed[-5:];recentids={t for _,s in recent for t in s['ids']}
        recentmet=metric(run,sorted(recentids,key=lambda t:run['byid'][t]['order']))
        hist=[e for e in es if e['monotonic_ns']<=cut]
        futures={t for o in ocs[n:] for t in o['segments']['B']['ids']}
        fe=[e for e in es if e['transition_id'] in futures and e['monotonic_ns']>cut]
        fields=['target_key','concrete_key','activity_concrete_key','model_transition_key','ID_only_model_transition_key','concrete_transition_key']
        future=dict(occurrences=len(ocs)-n,events=len(fe),native_new_state=sum(e['new_state'] for e in fe))
        for f in fields:
            before={e[f] for e in hist if e[f]}
            if f=='target_key':before|={e['source_key'] for e in hist}
            future['new_'+f]=len({e[f] for e in fe if e[f]}-before)
        opportunities=[]
        for row in inv['snapshots']:
            for mid in row['unconfirmed_since_observation']:
                hits=[e for e in fe if e['source_key']==row['source_key'] and e['input_model_action']==mid]
                if hits:
                    e=hits[0];opportunities.append(dict(source_key=row['source_key'],ModelAction=mid,
                        prior_selection_line=row['selection_line'],prior_available_ns=row['available_ns'],
                        first_future_execution=e['transition_id'],label=e['action_label'],
                        first_local_concrete=e['first_local_concrete'],first_ID_transition=e['first_ID_only_model_transition'],native_new_state=e['new_state']))
        future['later_executed_historical_ModelAction_opportunities']=opportunities
        future['discovery_events']=[e['transition_id'] for e in fe if e['new_state'] or e['first_ID_only_model_transition'] or e['first_local_concrete']]
        sigs=[key(*[(run['byid'][t]['source_key'],run['byid'][t]['concrete_key'] or run['byid'][t]['action_type'],run['byid'][t]['target_key']) for t in s['ids']]) for _,s in completed]
        out.append(dict(id=key(run['id'],fn,frac),run=run['id'],function=fn,fraction=frac,
            cutoff_entry=cutentry['transition_id'],cutoff_ns=cut,cutoff_wall_ms=cutentry['wall_time_ms'],past_entry_count=n,
            history_ids=sorted(ids,key=lambda t:run['byid'][t]['order']),history=metric(run,[e['transition_id'] for e in pe]),
            recent_completed_count=len(recent),recent_completed_entries=[o['entry'] for o,_ in recent],recent_metrics=recentmet,
            completed_occurrences=len(completed),incomplete_or_ongoing=n-len(completed),
            repeated_completed_paths=len(sigs)-len(set(sigs)),inventory=inv,
            feature_gui_snapshots_logged_by_cutoff=sum(g['monotonic_ns']<=cut and key(g['abstraction_version'],g['state_id']) in {e['source_key'] for e in pe} for g in run['guis']),
            future_validation=future,no_future_features=True))
    return out

def action_ledger(run,fn,ocs):
    members=defaultdict(list)
    ids={t for o in ocs for t in o['segments']['B']['ids'][1:]}
    for e in run['events']:
        if e['transition_id'] in ids and e['concrete_key']:members[e['concrete_key']].append(e)
    out=[]
    for k,es in members.items():
        first=es[0];targets=Counter(e['target_key'] for e in es)
        out.append(dict(run=run['id'],function=fn,local_concrete_key=k,activity_concrete_key=first['activity_concrete_key'],
            source_state=first['state'],input_version=first['input_version'],ModelAction=first['input_model_action'],
            identity_digest=first['identity_digest'],resource_id=first['resource_id'],text=first['text'],content_desc=first['content_desc'],
            class_name=first['class'],label=first['action_label'],action_type=first['action_type'],action_class=first['action_class'],
            confirmed_executions=len(es),first_execution=es[0]['transition_id'],first_ns=es[0]['monotonic_ns'],
            last_execution=es[-1]['transition_id'],targets=dict(targets),result_count=len(targets),
            planned_input_texts=sorted({str(e['planned_input_text']) for e in es if e['planned_input_text'] is not None}),
            transition_ids=[e['transition_id'] for e in es],operation_executed=True,all_results_explored='UNKNOWN',
            available_but_untried_Concrete=None))
    return out

def sharing(run,groups):
    # Same Activity + exact Concrete identity is the project's existing related key.
    membership=defaultdict(set)
    for fn,ocs in groups.items():
        for o in ocs:
            for t in o['segments']['B']['ids'][1:]:membership[t].add(fn)
    stores={f:defaultdict(list) for f in ['concrete_key','activity_concrete_key']};out=[]
    for e in run['events']:
        for f,store in stores.items():
            if not e[f]:continue
            other=[x for x in store[e[f]] if membership[x['transition_id']]-membership[e['transition_id']]]
            if membership[e['transition_id']] and other:
                targets={x['target_key'] for x in other}
                out.append(dict(run=run['id'],level='L1' if f=='concrete_key' else 'L2',key=e[f],
                    event=e['transition_id'],function_groups=sorted(membership[e['transition_id']]),label=e['action_label'],
                    resource_id=e['resource_id'],action_class=e['action_class'],target=e['target_key'],
                    prior_other_groups=sorted(set().union(*(membership[x['transition_id']] for x in other))),
                    prior_execution_ids=[x['transition_id'] for x in other],unseen_other_group_target=e['target_key'] not in targets,
                    object_and_mode_equivalence='UNKNOWN',reusable='confirmed operation exemplar only, not result completeness'))
            if membership[e['transition_id']]:store[e[f]].append(e)
    return out

def main():
    prior=lines(STUDY4/'analysis/executions.jsonl');manifest=read(ROOT/'evidence/archive-manifest.json')
    write('analysis/protocol.json',dict(entry_labels='study4 frozen exploratory labels; no new semantic classifier',
        selected_input_not_inventory=True,final_graph_feature_usage=False,remaining_Concrete_unknown=None,
        inventory_levels={'EXACT':'complete time-aligned Native eligible control set, all membership/validity/strategy stages recorded',
            'PARTIAL':'at least selected concrete or same-GUI retained node observations; completeness unproved',
            'INFERRED':'only model telemetry/State transition, no concrete inventory members',
            'MISSING':'neither usable concrete observation nor aligned model telemetry'},
        GUI_availability='retrospective same GUI and timestamp separately from logged_by_input/cutoff',
        scopes='reviewed Activity/page envelopes; other frozen functions use conservative same-Activity segments, not complete functional Regions',
        counts='confirmed source visits, confirmed arrivals and SELECTION_DECISION denominators separate',
        step_windows='A2/A5/A10/A20 include entry plus subsequent N confirmed steps; B boundary retains exit result',
        no_new_experiment=True,no_online_changes=True))
    quality=[];visits=[];summaries=[];occurrences=[];checks=[];ledger=[];shared=[];guirows=[];decisions=[];runs=[];allgroups=[];diversity=[];contexts=[]
    for meta in manifest:
        run=load_run(meta,prior);runs.append(run);q,v=audit_inventory(run);quality.append(q);visits.extend(v)
        for s in run['selections']:
            known_ids=sorted((set(s['unvisited_model_action_ids'].split(','))-{''})|({s['model_action_id']} if s['model_action_id'] else set()))
            decisions.append(dict(run=run['id'],event_line=s['event_line'],step=s['step'],monotonic_ns=s['monotonic_ns'],version=s['abstraction_version'],
                state=s['state_id'],model_action_count=s['model_action_count'],enabled_valid_count=s['enabled_valid_action_count'],
                enabled_valid_unvisited_count=s['enabled_valid_unvisited_action_count'],unvisited_ids=s['unvisited_model_action_ids'],
                selected_ModelAction=s['model_action_id'],selected_resolved_node_count=s.get('selected_resolved_node_count'),
                evidence_level='PARTIAL' if s.get('selected_resolved_node_count',0)>0 else 'INFERRED',
                known_model_ids=known_ids,complete_model_ids=len(known_ids)==s['model_action_count'],
                model_count_observation='EXACT recorded telemetry; ModelAction properties/member inventory not complete',remaining_Concrete=None))
        for g in run['guis']:
            guirows.append(dict(run=run['id'],gui_id=g['gui_id'],timestamp=g['gui_timestamp'],state=g['state_id'],version=g['abstraction_version'],
                activity=g['activity'],available_ns=g['monotonic_ns'],path=g['evidence_path'],coverage=g['coverage'],
                evidence_level='PARTIAL',candidates=candidates(g)))
        frozen=defaultdict(list)
        for e in run['events']:
            if e['label']['category']=='Ⅰ' and e['label']['function']:frozen[e['label']['function']].append(e)
        groups={}
        for fn,ee in frozen.items():
            allgroups.append(dict(run=run['id'],function=fn,executions=len(ee),source_states=sorted({e['state'] for e in ee}),
                concrete_identities=len({e['identity_digest'] for e in ee}),transition_ids=[e['transition_id'] for e in ee],
                inherited_label='exploratory, not human gold',deep_audit=fn.split('.',1)[1] in FOCUS[run['app']]))
            if fn.split('.',1)[1] not in FOCUS[run['app']]:continue
            oo=[]
            for e in ee:
                modes={m:segment(run,e,m) for m in ['B']+['A'+str(n) for n in WINDOWS]}
                o=dict(id=run['id']+':'+e['transition_id'],run=run['id'],function=fn,entry=e['transition_id'],order=e['order'],
                    monotonic_ns=e['monotonic_ns'],source_state=e['state'],target_state=e['target_state'],identity_digest=e['identity_digest'],
                    segments=modes,evidence_level=e['inventory_level'],core=fn in CORE)
                for s in modes.values():s['metrics']=metric(run,s['ids'])
                oo.append(o);occurrences.append(o)
            groups[fn]=oo;ids={t for o in oo for t in o['segments']['B']['ids']};afterids={t for o in oo for t in o['segments']['B']['ids'][1:]}
            pe=[e for e in run['events'] if e['transition_id'] in ids]
            sig=Counter(key(*[(run['byid'][t]['source_key'],run['byid'][t]['concrete_key'] or run['byid'][t]['action_type'],run['byid'][t]['target_key']) for t in o['segments']['B']['ids']]) for o in oo if o['segments']['B']['complete'])
            summaries.append(dict(run=run['id'],function=fn,entry_executions=len(oo),source_states=len({e['state'] for e in ee}),
                concrete_entry_identities=len({e['identity_digest'] for e in ee}),strict_local_entry_keys=len({e['concrete_key'] for e in ee}),
                complete_segments=sum(o['segments']['B']['complete'] for o in oo),stops=dict(Counter(o['segments']['B']['stop'] for o in oo)),
                max_repeated_complete_path=max(sig.values(),default=0),extra_duplicate_complete_paths=sum(v-1 for v in sig.values()),
                functional_source_same_GUI_snapshots=sum(bool(e['retained_source_gui_paths']) for e in pe),
                downstream_same_GUI_snapshots=sum(bool(e['retained_source_gui_paths']) for e in pe if e['transition_id'] in afterids),
                functional_GUI_snapshots=len({p for e in pe for p in e['retained_source_gui_paths']}),
                current_untried_Concrete=None,coverage_fraction=None,inventory_level='PARTIAL',
                **metric(run,[e['transition_id'] for e in pe])))
            checks.extend(checkpoints(run,fn,oo));ledger.extend(action_ledger(run,fn,oo))
        shared.extend(sharing(run,groups))
        ma=defaultdict(list);acts=defaultdict(list)
        for e in run['events']:
            if e['concrete_key']:ma[key(e['input_version'],e['state'],e['input_model_action'])].append(e)
            acts[e['source_activity']].append(e)
        for k,ee in ma.items():
            diversity.append(dict(run=run['id'],version_State_ModelAction=k,executions=len(ee),
                executed_concrete_identity_count=len({e['identity_digest'] for e in ee}),
                concrete_identities=sorted({e['identity_digest'] for e in ee}),resource_ids=sorted({e['resource_id'] for e in ee}),
                texts=sorted({e['text'] for e in ee}),node_paths=sorted({e['node_path'] for e in ee}),
                target_states=sorted({e['target_key'] for e in ee}),transition_ids=[e['transition_id'] for e in ee],
                scope='executed history only; no simultaneous inventory or semantic equivalence claim'))
        for a,ee in acts.items():
            contexts.append(dict(run=run['id'],activity=a,confirmed_source_visits=len(ee),
                selected_concrete_identities=len({e['identity_digest'] for e in ee if e['identity_digest']}),
                source_same_GUI_visits=sum(bool(e['retained_source_gui_paths']) for e in ee),
                logged_GUI_snapshots=sum(g['activity']==a for g in run['guis']),
                selected_only_PARTIAL=sum(e['inventory_level']=='PARTIAL' for e in ee),
                complete_valid_inventory_EXACT=0,remaining_Concrete=None,
                first_input=ee[0]['transition_id'],last_input=ee[-1]['transition_id']))
        print(run['id'],len(run['events']),'confirmed;',len(run['guis']),'GUI;',len(groups),'audited groups',flush=True)
    for name,rows in [('archive-inventory-coverage',quality),('source-visit-inventory',visits),('decision-inventory',decisions),
        ('frozen-entry-groups',allgroups),('function-summary',summaries),('entry-occurrences',occurrences),
        ('temporal-checkpoints',checks),('functional-action-result-ledger',ledger),('cross-entry-transfer',shared),('GUI-flag-observations',guirows),
        ('model-action-concrete-diversity',diversity),('activity-context-coverage',contexts)]:safe_table(name,rows)
    with (ROOT/'analysis/events.jsonl').open('w') as f:
        for run in runs:
            for e in run['events']:f.write(json.dumps(e,ensure_ascii=False,separators=(',',':'))+'\n')
    write('analysis/run-metadata.json',[dict(id=r['id'],meta=r['meta'],base=r['base']) for r in runs])
    write('evidence/read-source-sha256.json',READ_FILES)
    write('analysis/summary.json',dict(runs=len(runs),confirmed_executions=sum(len(r['events']) for r in runs),
        decision_points=len(decisions),GUI_snapshots=len(guirows),deep_function_run_groups=len(summaries),entry_occurrences=len(occurrences),
        checkpoints=len(checks),action_result_ledger_rows=len(ledger),exact_full_executable_inventories=0,
        sharing_events=len(shared),no_future_features=True))

if __name__=='__main__':main()
