package com.android.commands.monkey.ape.jev;

import java.util.*;
import org.json.*;
import android.graphics.PointF;
import android.graphics.Rect;
import com.android.commands.monkey.ape.events.ApeTouchGeometry;
import com.android.commands.monkey.ape.tree.GUITree;
import com.android.commands.monkey.ape.tree.GUITreeAction;
import com.android.commands.monkey.ape.tree.GUITreeNode;
import com.android.commands.monkey.ape.model.State;
import com.android.commands.monkey.ape.model.ModelAction;
import static com.android.commands.monkey.ape.jev.JevProtocol.AFTER_SATURATION;

/** Main-thread-only derived bookkeeping. Raw native transitions remain owned by APE. */
public final class JevRegistry {
    public enum Status { UNSENT, WAITING_HISTORY, QUEUED, REPRESENTED, PROBE_SUGGESTED, RETIRED, DELIVERY_FAILED, EXECUTED }
    public static final String SAMPLING="members:candidate-coverage-first-then-concrete-similarity-then-transition-id;cross-state:action-type+activity+fullName;dedup:concrete-identity;prompt:no-execution-results;limits:8/4";
    /** Evidence fields; stable concrete identity is supplied by ConcreteActionIdentity. */
    public static final class Member {
        public final String type, labelsJson, roleSignatureJson, localContextJson;
        final String fullName;
        public final boolean unsafe;
        public Member(String type,JSONArray labels,JSONArray roleSignature,String fullName,boolean unsafe) {
            this(type,labels,roleSignature,fullName,unsafe,null);
        }
        public Member(String type,JSONArray labels,JSONArray roleSignature,String fullName,boolean unsafe,JSONObject localContext) {
            this.type=type;labelsJson=labels.toString();roleSignatureJson=roleSignature.toString();
            localContextJson=localContext==null?null:localContext.toString();this.fullName=fullName;this.unsafe=unsafe;
        }
        JSONObject view(String idField,String id) throws JSONException {
            JSONObject out=new JSONObject().put(idField,id).put("target_subtree_labels",new JSONArray(labelsJson))
                .put("role_signature",new JSONArray(roleSignatureJson));
            out.put("local_context",localContextJson==null?JSONObject.NULL:new JSONObject(localContextJson));
            return out;
        }
        JSONObject historyView(String idField,String id) throws JSONException {
            return new JSONObject().put(idField,id);
        }
    }
    public static final class Observation {
        public final String state,action,type,sourceActivity;
        public final GUITree sourceTree;
        public final GUITreeAction concreteAction;
        public final List<GUITreeNode> concreteNodes;
        public final boolean valid,saturated;
        public final List<Member> members;
        public Observation(String state,String action,String type,String sourceActivity,GUITree sourceTree,GUITreeAction concreteAction,
                List<GUITreeNode> concreteNodes,boolean valid,boolean saturated,List<Member> members) {
            this.state=state;this.action=action;this.type=type;this.sourceActivity=sourceActivity==null?"":sourceActivity;
            this.sourceTree=sourceTree;this.concreteAction=concreteAction;
            this.concreteNodes=Collections.unmodifiableList(new ArrayList<GUITreeNode>(concreteNodes));
            this.valid=valid;this.saturated=saturated;this.members=members;
        }
    }
    public static final class Entry {
        public long version;
        public final String id;
        public String state,action;
        public final String sourceActivity;
        public final String reviewSource=AFTER_SATURATION;
        public boolean structural;
        boolean additionalAdmitted,probeConfirmed,naturalConfirmed,rebuildMatched,probeClaimed;
        String terminalReason;
        public final GUITree sourceTree;
        public final GUITreeNode node;
        public final GUITreeAction concreteAction;
        public Member member;
        public Status status=Status.QUEUED;
        public String requestId;
        public Double confidence;
        public long decisionStep,decisionNanos;
        boolean unsentLogged;
        Entry(long v,String id,String s,String a,GUITree sourceTree,GUITreeAction concreteAction,Member m,String activity) {
            version=v;this.id=id;state=s;action=a;sourceActivity=activity==null?"":activity;
            this.sourceTree=sourceTree;this.concreteAction=concreteAction;this.node=concreteAction==null?null:concreteAction.getGUITreeNode();
            member=m;
        }
    }
    public static final class Batch {
        public final long version;
        public final String id,state,action,stateJson,reviewSource;
        public final List<Entry> entries;
        public final List<Execution> history;
        public final Map<String,Execution> executedSiblings;
        public final boolean unsafe;
        public JevProtocol.Request request;
        Map<String,Entry> semanticEntries=Collections.emptyMap();
        List<Batch> pageGroups=Collections.emptyList();
        private boolean completed;
        Batch(long v,String id,String s,String a,List<Entry> es,String json,List<Execution> history,Map<String,Execution> siblingKeys,boolean unsafe) {
            version=v;this.id=id;state=s;action=a;entries=Collections.unmodifiableList(new ArrayList<Entry>(es));
            stateJson=json;this.history=Collections.unmodifiableList(new ArrayList<Execution>(history));this.unsafe=unsafe;
            reviewSource=es.get(0).reviewSource;
            executedSiblings=Collections.unmodifiableMap(new LinkedHashMap<String,Execution>(siblingKeys));
        }
    }
    static final class Outcome {
        final String state,activity,labels,sourceContext,localChange; final Member sourceMember; int count;
        final boolean selfLoop,activityUnchanged;final Boolean guiUnchanged;
        Outcome(String s,String a,String l,boolean loop,boolean sameActivity,Boolean sameGui,Member source,String context,String change) {
            state=s;activity=a;labels=l;selfLoop=loop;activityUnchanged=sameActivity;guiUnchanged=sameGui;sourceMember=source;sourceContext=context;localChange=change;
        }
        JSONObject view(String contextId) throws JSONException {return new JSONObject().put("source_snapshot",new JSONObject()
            .put("source_context_id",contextId).put("target_subtree_labels",new JSONArray(sourceMember.labelsJson))
            .put("role_signature",new JSONArray(sourceMember.roleSignatureJson)).put("local_context",sourceMember.localContextJson==null?JSONObject.NULL:new JSONObject(sourceMember.localContextJson)))
            .put("target_activity",JevEvidence.boundedText(activity)).put("target_state",state)
            .put("count",count).put("result_labels",new JSONArray(labels)).put("self_loop",selfLoop)
            .put("activity_unchanged",activityUnchanged).put("gui_unchanged",guiUnchanged==null?JSONObject.NULL:guiUnchanged)
            .put("local_change",localChange==null?JSONObject.NULL:new JSONObject(localChange));}
    }
    static final class Execution {
        final Member member; int count; boolean unsafe;
        final Set<String> sourceActivities=new HashSet<String>();
        final Anchor anchor;
        final GUITreeAction concreteAction;
        final SortedMap<String,Outcome> outcomes=new TreeMap<String,Outcome>();
        final List<String> transitions=new ArrayList<String>();
        Execution(Member m,String sourceActivity,Anchor anchor,GUITreeAction concreteAction) {
            member=m;unsafe=m.unsafe;this.anchor=anchor;this.concreteAction=concreteAction;
            sourceActivities.add(sourceActivity==null?"":sourceActivity);
        }
    }
    /** APE-owned exact execution anchor; this is native scope, not review identity. */
    static final class Anchor {
        final GUITree tree; final GUITreeNode node; final String type;
        Anchor(GUITree tree,GUITreeNode node,String type) {this.tree=tree;this.node=node;this.type=type;}
        public int hashCode() {return System.identityHashCode(tree)*31*31+System.identityHashCode(node)*31+type.hashCode();}
        public boolean equals(Object o) {if(!(o instanceof Anchor))return false;Anchor a=(Anchor)o;return tree==a.tree && node==a.node && type.equals(a.type);}
    }
    private long version=-1,entrySeq,requestSeq;
    /** Read the existing per-action execution index; unrelated States do not invalidate work. */
    long executionRevision(State state) {
        long count=0;
        for(ModelAction action:state.getActions()) {
            JevDeadline.check();List<Execution> values=executionIndex.get(new ActionKey(state,action));
            if(values!=null)for(Execution value:values){JevDeadline.check();count+=value.count;}
        }
        return count;
    }
    /** Revision for one Pair's source ModelAction; unrelated actions in the
        same State must not invalidate an otherwise identical Pair. */
    long executionRevision(State state,ModelAction action) {
        long profileStart=JevReviewProfile.start();
        try {
        if(state==null||action==null)return 0;
        long count=0;List<Execution> values=executionIndex.get(new ActionKey(state,action));
        if(values!=null)for(Execution value:values){JevDeadline.check();count+=value.count;}
        return count;
        } finally {JevReviewProfile.end("execution_revision",profileStart);}
    }
    private String syncReviewContract="sync-v4";
    public void setSemanticReviewModel(String model){syncReviewContract="semantic-v1:"+(model==null?"":model);}
    private String requestNamespace="";
    public void requestNamespace(String value) {
        if(value==null || !value.matches("[A-Za-z0-9_-]+"))throw new IllegalArgumentException("Invalid branch request namespace");
        if(requestSeq!=0)throw new IllegalStateException("Cannot rename submitted request history");
        requestNamespace=value+"-";
    }
    private String nextRequestId(){return requestNamespace+"r"+(++requestSeq);}
    /** Read-only diagnostic view of the exact index used by candidate identity matching. */
    List<Execution> historyAuditExecutions(State state,ModelAction action) {
        List<Execution> values=executionIndex.get(new ActionKey(state,action));
        return values==null?Collections.<Execution>emptyList():Collections.unmodifiableList(values);
    }
    public JSONObject historyEvidence() {
        int observations=0;for(Execution execution:concreteExecutions.values())observations+=execution.count;
        return fields("concrete_executions",concreteExecutions.size(),"execution_observations",observations,
            "request_count",requestSeq,"advice_count",audits.size());
    }
    public void rebuildHistory(long v,long step) {
        if(!audits.isEmpty() || requestSeq!=0)throw new IllegalStateException("Rebuild only at unsent checkpoint");
        concreteExecutions.clear();executionIndex.clear();relatedIndex.clear();executionOrder.clear();executionOrderDirty=false;
        version=v;
    }
    private final LinkedHashMap<Anchor,Entry> audits=new LinkedHashMap<Anchor,Entry>();
    /** Every entry is one confirmed native transition for one APE concrete anchor. */
    private final Map<Anchor,Execution> concreteExecutions=new HashMap<Anchor,Execution>();
    // Neither cache level keeps a tree alive through a node's DOM/document back-reference.
    // A present null is a cached negative structural result, independent of audit status.
    private final Map<GUITree,Map<GUITreeNode,Map<String,JSONObject>>> structures=
        new WeakHashMap<GUITree,Map<GUITreeNode,Map<String,JSONObject>>>();
    private static final class ActionKey {
        final State state; final ModelAction action;
        ActionKey(GUITreeAction concrete){state=concrete.getGUITree().getCurrentState();action=concrete.getModelAction();}
        ActionKey(State state,ModelAction action){this.state=state;this.action=action;}
        public int hashCode(){return System.identityHashCode(state)*31+System.identityHashCode(action);}
        public boolean equals(Object other){if(!(other instanceof ActionKey))return false;ActionKey k=(ActionKey)other;return state==k.state && action==k.action;}
    }
    private final Map<ActionKey,List<Entry>> auditIndex=new HashMap<ActionKey,List<Entry>>();
    private final Map<ConcreteActionIdentity,List<Entry>> identityAudits=new HashMap<ConcreteActionIdentity,List<Entry>>();
    private final Map<ActionKey,List<Execution>> executionIndex=new HashMap<ActionKey,List<Execution>>();
    private final Map<String,List<Execution>> relatedIndex=new HashMap<String,List<Execution>>();
    private final Map<String,Entry> admittedProbes=new LinkedHashMap<String,Entry>();
    private boolean replaying,executionOrderDirty;
    private final Map<Execution,Integer> executionOrder=new IdentityHashMap<Execution,Integer>();
    private final Map<String,Entry> entriesById=new HashMap<String,Entry>();
    private final JevReviewLedger reviewLedger=new JevReviewLedger();
    interface ChangeListener {void changed(ModelAction action);}
    private ChangeListener changes;
    private JevTiming timing=new JevTiming();
    void observeChanges(ChangeListener listener,JevTiming timing){changes=listener;this.timing=timing;}
    private void changed(ModelAction action){if(changes!=null)changes.changed(action);}
    private static <T> void addToIndex(Map<ActionKey,List<T>> index,GUITreeAction action,T value) {
        ActionKey key=new ActionKey(action);List<T> values=index.get(key);
        if(values==null){values=new ArrayList<T>();index.put(key,values);}values.add(value);
    }
    private static <T> List<T> indexed(Map<ActionKey,List<T>> index,GUITreeAction action) {
        List<T> values=index.get(new ActionKey(action));return values==null?Collections.<T>emptyList():values;
    }
    List<Entry> actionEntries(ModelAction action,GUITree tree){
        List<Entry> out=new ArrayList<Entry>(indexed(auditIndex,new GUITreeAction(tree,null,action)));
        GUITreeNode[] nodes=action==null?null:action.getResolvedNodes();
        if(nodes!=null)for(GUITreeNode node:nodes) {
            ConcreteActionIdentity identity=ConcreteActionIdentity.of(action.getType().name(),node);List<Entry> matches=identityAudits.get(identity);
            if(matches==null)continue;
            for(Entry entry:matches)if(entry.sourceActivity.equals(activityScope(tree))&&!out.contains(entry))out.add(entry);
        }
        return out;
    }
    Entry entry(String id){return entriesById.get(id);}
    public List<Entry> pendingEntries(ModelAction action,GUITree tree) {
        List<Entry> out=new ArrayList<Entry>();for(Entry e:actionEntries(action,tree))if(probeAvailable(e))out.add(e);return out;
    }
    private final Set<String> seenSaturation=new HashSet<String>(),historyDeferredLogged=new HashSet<String>();
    private final JevEvents events;
    private final double localProbeConfidence;
    public static final double NAVIGATION_CONFIDENCE=0.8;
    private long dedupCount,unsentDeferrals;
    public JevRegistry(JevEvents events,double threshold) {
        if(Double.isNaN(threshold) || threshold<0 || threshold>1)throw new IllegalArgumentException("invalid confidence threshold");
        this.events=events;localProbeConfidence=threshold;
    }
    static String group(long v,String s,String a) {return new JSONArray().put(v).put(s).put(a).toString();}
    private static String activityScope(GUITree tree) {if(tree==null)return "";String value=tree.getActivityClassName();return value==null?"":value;}
    public long version() {return version;}
    public JevReviewLedger reviewLedger(){return reviewLedger;}
    public static JSONObject fields(Object... pairs) {
        JSONObject j=new JSONObject();
        try {for(int i=0;i<pairs.length;i+=2) j.put((String)pairs[i],pairs[i+1]);}
        catch(JSONException e) {throw new IllegalStateException(e);} return j;
    }
    private JSONObject entryFields(Entry e) {return fields("entry_id",e.id,"request_id",e.requestId==null?JSONObject.NULL:e.requestId,"review_source",e.reviewSource);}
    private void event(String name,Entry e,long step,JSONObject f) {
        changed(e.concreteAction.getModelAction());events.emit(name,step,e.version,e.state,e.action,f);
        if(e.additionalAdmitted) {
            if("DECISION_REPRESENTED".equals(name)) {e.terminalReason="failed_or_interrupted";lifecycle(e,step,"superseded_by_representation",false);}
            if("PROBE_EXECUTION_FAILED".equals(name) || "anchor_unresolved_after_refinement".equals(f.optString("reason")))e.terminalReason="failed_or_interrupted";
        }
    }
    public void reset(long v,long step) {
        if(v==version)return;
        // APE refinement replaces graph objects. Review decisions and confirmed
        // review facts belong to the run-level ledger; native execution facts
        // are rebuilt from APE's transition history by JevRuntime.
        version=v;
        for(Entry e:audits.values())e.version=v;
        concreteExecutions.clear();executionIndex.clear();relatedIndex.clear();
        executionOrder.clear();executionOrderDirty=false;
        seenSaturation.clear();historyDeferredLogged.clear();
    }

    /** End a selection round while retaining reusable review decisions. */
    public void discardAdvice(Entry executing,long step) {
        for(Entry e:audits.values())if(e!=executing && e.status==Status.PROBE_SUGGESTED)e.probeClaimed=false;
    }

    public List<Entry> suggestions(State state,GUITree tree) {
        List<Entry> out=new ArrayList<Entry>();
        for(ModelAction action:state.getActions())for(Entry e:actionEntries(action,tree))
            if(probeAvailable(e) && JevEvidence.guiIdentity(e.sourceTree).equals(JevEvidence.guiIdentity(tree))
                && correspondingNode(e.concreteAction,tree,action)!=null)out.add(e);
        Collections.sort(out,new Comparator<Entry>(){public int compare(Entry a,Entry b){
            int c=Double.compare(b.confidence,a.confidence);return c!=0?c:a.id.compareTo(b.id);
        }});
        List<Entry> unique=new ArrayList<Entry>();
        for(Entry e:out){boolean duplicate=false;for(Entry old:unique)if(sameConcrete(e.concreteAction,old.concreteAction)){duplicate=true;break;}
            if(!duplicate)unique.add(e);}
        return unique;
    }
    static JSONObject merge(JSONObject a,JSONObject b) {
        try {Iterator<?> it=b.keys();while(it.hasNext()){String k=(String)it.next();a.put(k,b.get(k));}}
        catch(JSONException e){throw new IllegalStateException(e);}return a;
    }
    public Status status(GUITree tree,GUITreeNode node,String actionType) {
        Anchor a=anchor(tree,node,actionType);if(a==null)return null;
        if(concreteExecutions.containsKey(a))return Status.EXECUTED;
        Entry e=audits.get(a);return e==null?null:e.status;
    }

    // A read-only reference to the existing runtime authority, not another execution index.
    private com.android.commands.monkey.ape.model.Model confirmationModel;
    private Set<GUITreeAction> confirmedInputs=Collections.emptySet();
    private boolean confirmationTrackingStarted,confirmationHistoryComplete;
    void confirmationSource(com.android.commands.monkey.ape.model.Model model,Set<GUITreeAction> inputs) {
        if(!confirmationTrackingStarted) {
            confirmationHistoryComplete=model!=null && model.getGraph().getTreeHistory().isEmpty();
            if(model!=null)for(com.android.commands.monkey.ape.model.Model.ActionRecord record:model.getActionHistory())
                if(record.modelAction.isModelAction() && record.modelAction.requireTarget())confirmationHistoryComplete=false;
            confirmationTrackingStarted=true;
        }
        // Native refinement mutates this Model in place. An unrelated replacement
        // cannot inherit the original continuous-confirmation completeness claim.
        else if(confirmationModel!=model || confirmedInputs!=inputs)confirmationHistoryComplete=false;
        confirmationModel=model;confirmedInputs=inputs;
    }
    static final class ConfirmedHistory {
        final IdentityHashMap<ModelAction,List<GUITreeAction>> groups=new IdentityHashMap<ModelAction,List<GUITreeAction>>();
        boolean complete;int total,unknown;long enumerationNanos;
        List<GUITreeAction> members(ModelAction action) {
            List<GUITreeAction> found=groups.get(action);
            return found==null?Collections.<GUITreeAction>emptyList():found;
        }
    }
    ConfirmedHistory confirmedHistory(State state) {
        long profileStart=JevReviewProfile.start();
        long begin=System.nanoTime();ConfirmedHistory view=new ConfirmedHistory();
        int before=confirmedInputs.size();
        view.complete=confirmationHistoryComplete && confirmationModel!=null && confirmationModel.getVersion()==version
            && JevHistoryAudit.identityContains(confirmationModel.getGraph().getStates(),state);
        if(confirmationModel==null){view.enumerationNanos=System.nanoTime()-begin;JevReviewProfile.end("history_scan_group_sort",profileStart);return view;}
        for(GUITreeAction input:confirmedInputs) {
            JevDeadline.check();view.total++;
            if(confirmedMappingProblem(input)!=null){view.unknown++;continue;}
            if(input.getGUITree().getCurrentState()!=state)continue;
            ModelAction action=input.getModelAction();
            if(!action.requireTarget())continue;
            List<GUITreeAction> group=view.groups.get(action);
            if(group==null){group=new ArrayList<GUITreeAction>();view.groups.put(action,group);}group.add(input);
        }
        for(List<GUITreeAction> group:view.groups.values())Collections.sort(group,new Comparator<GUITreeAction>() {
            public int compare(GUITreeAction a,GUITreeAction b) {
                int c=Integer.compare(a.getGUITree().getTimestamp(),b.getGUITree().getTimestamp());
                if(c==0)c=Integer.compare(a.getGUITree().getId(),b.getGUITree().getId());
                if(c==0)c=ConcreteActionIdentity.of(a.getActionType().name(),a.getGUITreeNode()).toString()
                    .compareTo(ConcreteActionIdentity.of(b.getActionType().name(),b.getGUITreeNode()).toString());
                return c;
            }
        });
        view.complete &= view.unknown==0 && before==confirmedInputs.size() && confirmationModel.getVersion()==version;
        view.enumerationNanos=System.nanoTime()-begin;JevReviewProfile.end("history_scan_group_sort",profileStart);return view;
    }
    int confirmationCount(){return confirmedInputs.size();}
    private boolean confirmedExact(GUITreeAction candidate) {
        long profileStart=JevReviewProfile.start(),examined=0;
        try {
        if(confirmationModel==null)return false;
        for(GUITreeAction prior:confirmedInputs) {
            examined++;
            JevDeadline.check();
            if(prior.getModelAction()!=candidate.getModelAction())continue;
            if(confirmedMappingProblem(prior)==null && sameConcrete(candidate,prior))return true;
        }
        return false;
        } finally {JevReviewProfile.end("confirmed_exact",profileStart);JevReviewProfile.add("confirmed_inputs_examined",0,examined);}
    }

    /** Exact execution in the current State/tree. This never consults the cross-State index. */
    public boolean hasExactExecution(State state,ModelAction action,GUITree tree,GUITreeNode node) {
        if(state==null || action==null || tree==null || node==null || tree.getCurrentState()!=state)return false;
        long profileStart=JevReviewProfile.start();
        try {GUITreeAction candidate=new GUITreeAction(tree,node,action);
            return hardMatch(candidate)!=null || confirmedExact(candidate);
        } finally {JevReviewProfile.end("exact_execution_filter",profileStart);}
    }

    /** Confirmed concrete execution in any State of the same activity. */
    public boolean hasRelatedConcreteExecution(String actionType,String sourceActivity,String canonicalFullName) {
        if(actionType==null || sourceActivity==null || canonicalFullName==null || canonicalFullName.length()==0)return false;
        ConcreteActionIdentity identity=new ConcreteActionIdentity(actionType,canonicalFullName);
        List<Execution> values=relatedIndex.get(relatedKey(sourceActivity,identity));
        if(values!=null && !values.isEmpty())return true;
        // Input may be confirmed before its transition is observed/indexed.
        // Consult the existing authority; do not invent another history index.
        for(GUITreeAction input:confirmedInputs) {
            if(input.getGUITreeNode()!=null && confirmedMappingProblem(input)==null && input.getActionType().name().equals(actionType)
                    && sourceActivity.equals(input.getModelAction().getState().getActivity())
                    && identity.equals(ConcreteActionIdentity.of(actionType,input.getGUITreeNode())))return true;
        }
        return false;
    }
    private static String relatedKey(String activity,ConcreteActionIdentity identity) {
        return new JSONArray().put(identity.actionType()).put(activity).put(identity.canonicalFullName()).toString();
    }
    public List<Entry> suggestions() {
        List<Entry> out=new ArrayList<Entry>();
        for(Entry e:audits.values())if(probeAvailable(e))out.add(e);
        return out;
    }
    private boolean probePending(Entry e) {return e.status==Status.PROBE_SUGGESTED && !e.probeClaimed;}
    public boolean probeAvailable(Entry e) {return probePending(e) && e.confidence!=null && e.confidence>localProbeConfidence;}
    /** Reserve this concrete candidate only; siblings keep their independent opportunities. */
    public boolean claimProbe(Entry entry,long step) {
        if(!probeAvailable(entry))return false;
        entry.probeClaimed=true;
        event("PROBE_CLAIMED",entry,step,merge(entryFields(entry),fields("reason","binding_attempt","selection_version",version)));
        return true;
    }
    public void recordProbeExecution(Entry entry,long step) {
        entry.probeConfirmed=true;entry.status=Status.EXECUTED;
        lifecycle(entry,step,"probe_executed",false);
    }
    public List<Batch> discover(List<Observation> observations,long step) throws JSONException {
        long start=System.nanoTime();try{return discoverNow(observations,step);}finally{timing.record("audit_discovery",System.nanoTime()-start);}
    }
    private List<Batch> discoverNow(List<Observation> observations,long step) throws JSONException {
        List<Batch> batches=new ArrayList<Batch>();
        for(Observation o:observations) {
            if(!o.valid || o.concreteAction==null || (!o.saturated || o.concreteAction.getModelAction().isUnvisited())
                    || "BACK".equals(o.type) || "MODEL_BACK".equals(o.type))continue;
            String source=AFTER_SATURATION;
            String g=group(version,o.state,o.action);
            boolean first=seenSaturation.add(g);
            if(first)events.emit("SATURATION_TRIGGER",
                step,version,o.state,o.action,fields("action_type",o.type));
            List<Entry> fresh=new ArrayList<Entry>();
            List<GUITreeAction> currentActions=new ArrayList<GUITreeAction>();
            Map<ConcreteActionIdentity,Integer> identityCounts=new HashMap<ConcreteActionIdentity,Integer>();
            for(int memberIndex=0;memberIndex<o.members.size();memberIndex++) {
                GUITreeNode node=memberIndex<o.concreteNodes.size()?o.concreteNodes.get(memberIndex):null;
                if(node!=null) {
                    ConcreteActionIdentity identity=ConcreteActionIdentity.of(o.members.get(memberIndex).type,node);
                    Integer count=identityCounts.get(identity);identityCounts.put(identity,count==null?1:count+1);
                }
            }
            for(int memberIndex=0;memberIndex<o.members.size();memberIndex++) {
                Member m=o.members.get(memberIndex);
                GUITreeNode concreteNode=memberIndex<o.concreteNodes.size()?o.concreteNodes.get(memberIndex):null;
                if(concreteNode!=null && !concreteNode.isEnabled())continue;
                if(concreteNode!=null && identityCounts.get(ConcreteActionIdentity.of(m.type,concreteNode))>1) {
                    events.emit("CANDIDATE_REJECTED",step,version,o.state,o.action,fields("reason","ambiguous_concrete_identity"));
                    continue;
                }
                Anchor candidateAnchor=anchor(o.sourceTree,concreteNode,m.type);
                if(candidateAnchor==null || o.concreteAction==null || o.concreteAction.getModelAction()==null) {
                    events.emit("CANDIDATE_REJECTED",step,version,o.state,o.action,fields("reason","missing_concrete_anchor"));
                    continue;
                }
                GUITreeAction candidateAction=new GUITreeAction(o.sourceTree,concreteNode,o.concreteAction.getModelAction());
                currentActions.add(candidateAction);
                Execution hardMatch=hardMatch(candidateAction);
                if(hardMatch!=null) {
                    Entry stale=findAudit(candidateAction);
                    if(stale!=null)retireIfExecuted(stale,step);
                    dedupCount++;
                    events.emit("AUDIT_DEDUPLICATED",step,version,o.state,o.action,fields(
                        "reason","hard_exact_execution","transition_id",hardMatch.transitions.isEmpty()?JSONObject.NULL:hardMatch.transitions.get(0)));
                    continue;
                }
                Entry previous=findAudit(candidateAction);
                if(previous==null && unresolvedProbe(candidateAction))continue;
                if(previous!=null)revalidateStructure(previous,o.sourceTree,concreteNode,step);
                Entry e=previous;
                if(e==null) {
                    e=new Entry(version,"e"+(++entrySeq),o.state,o.action,o.sourceTree,candidateAction,m,o.sourceActivity);
                    audits.put(candidateAnchor,e);addToIndex(auditIndex,candidateAction,e);
                    ConcreteActionIdentity identity=ConcreteActionIdentity.of(m.type,concreteNode);
                    List<Entry> identityEntries=identityAudits.get(identity);
                    if(identityEntries==null){identityEntries=new ArrayList<Entry>();identityAudits.put(identity,identityEntries);}
                    identityEntries.add(e);entriesById.put(e.id,e);
                    if(!first)event("NEW_REALIZATION_TRIGGER",e,step,entryFields(e));
                    event("CANDIDATE_OBSERVED",e,step,entryFields(e));
                } else if(!e.reviewSource.equals(source) || (e.status!=Status.UNSENT && e.status!=Status.WAITING_HISTORY && !retryableReview(e))) {
                    dedupCount++;continue;
                }
                e.member=m;
                if(retryableReview(e)){e.status=Status.QUEUED;e.requestId=null;e.unsentLogged=false;}
                JevReviewLedger.Decision cached=reviewLedger.get(syncReviewContract,
                    o.sourceActivity,ConcreteActionIdentity.of(m.type,concreteNode));
                if(cached!=null) {
                    e.confidence=cached.confidence;
                    if("PROBE".equals(cached.outcome) && cached.confidence!=null && cached.confidence>localProbeConfidence)e.status=Status.PROBE_SUGGESTED;
                    else if("REPRESENTED".equals(cached.outcome))e.status=Status.REPRESENTED;
                    else e.status=Status.RETIRED;
                    dedupCount++;continue;
                }
                JSONObject structure=structuralRepresentation(o.sourceTree,concreteNode,o.type);
                if(structure!=null) {
                    e.status=Status.REPRESENTED;e.structural=true;
                    event("DECISION_REPRESENTED",e,step,merge(merge(entryFields(e),fields("represented_source","structure")),structure));
                    continue;
                }
                e.status=Status.QUEUED;
                fresh.add(e);
            }
            if(fresh.isEmpty())continue;
            List<Execution> historyExecutions=new ArrayList<Execution>();
            List<Member> freshMembers=new ArrayList<Member>();for(Entry e:fresh)freshMembers.add(e.member);
            ModelAction currentModelAction=o.concreteAction==null?null:o.concreteAction.getModelAction();
            State currentState=o.sourceTree==null?null:o.sourceTree.getCurrentState();
            JSONObject contexts=new JSONObject();
            JSONArray history=history(currentActions,o.type,o.sourceActivity,currentState,currentModelAction,freshMembers,historyExecutions,contexts);
            JSONArray siblings=new JSONArray();int siblingId=0;
            Map<String,Execution> siblingKeys=new LinkedHashMap<String,Execution>();
            Map<String,String> contextIds=new LinkedHashMap<String,String>();
            boolean siblingUnsafe=false;
            for(int memberIndex=0;memberIndex<o.members.size();memberIndex++) {
                Member m=o.members.get(memberIndex);String sid="s"+(++siblingId);
                GUITreeNode node=memberIndex<o.concreteNodes.size()?o.concreteNodes.get(memberIndex):null;
                Execution x=siblingHistory(new GUITreeAction(o.sourceTree,node,currentModelAction),m);
                if(x!=null) {
                    JSONObject view=m.historyView("sibling_id",sid).put("execution_count",x.count)
                        .put("outcomes",outcomes(x,new HashSet<String>(),contexts,contextIds))
                        .put("outcomes_truncated",x.outcomes.size()>4);
                    siblings.put(view);
                    siblingKeys.put(sid,x);siblingUnsafe|=x.unsafe;
                }
            }
            // Semantic attribution is page-local.  It does not require an
            // execution history or an observed result to establish ownership.
            String id=nextRequestId();
            JSONArray candidates=new JSONArray();boolean unsafe=siblingUnsafe;
            for(int i=0;i<fresh.size();i++) {Entry e=fresh.get(i);e.requestId=id;candidates.put(e.member.view("candidate_id","c"+(i+1)));unsafe|=e.member.unsafe;}
            for(Execution h:historyExecutions)unsafe|=h.unsafe;
            JSONObject state=fields("review_source",source,"candidate_review_rules",JevProtocol.CANDIDATE_RULES,"history_contexts",contexts,"source_context",JevEvidence.sourceContext(o.sourceTree),"group",fields("action_type",o.type),"candidates",candidates,"current_siblings",siblings,"executed_history_other_pages",history);
            Batch b=new Batch(version,id,o.state,o.action,fresh,state.toString(),historyExecutions,siblingKeys,unsafe);batches.add(b);

        }
        return batches;
    }
    private boolean retryableReview(Entry e) {
        if(e==null)return false;
        if(e.status==Status.DELIVERY_FAILED)return true;
        if(e.status!=Status.RETIRED || e.structural)return false;
        ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
        return reviewLedger.get(syncReviewContract,e.sourceActivity,identity)==null;
    }
    private JSONObject structuralRepresentation(GUITree tree,GUITreeNode candidate,String type) {
        if(tree==null || candidate==null)return null;
        Map<GUITreeNode,Map<String,JSONObject>> nodes=structures.get(tree);
        if(nodes==null){nodes=new WeakHashMap<GUITreeNode,Map<String,JSONObject>>();structures.put(tree,nodes);}
        Map<String,JSONObject> results=nodes.get(candidate);
        if(results==null){results=new HashMap<String,JSONObject>();nodes.put(candidate,results);}
        if(!results.containsKey(type)) {
            long start=System.nanoTime();results.put(type,computeStructure(tree,candidate,type,tree.getDisplayBounds()));
            timing.record("structure",System.nanoTime()-start);timing.count("structure_misses",1);
        } else timing.count("structure_hits",1);
        return results.get(type);
    }
    private static JSONObject computeStructure(GUITree tree,GUITreeNode candidate,String type,Rect display) {
        if(!"MODEL_CLICK".equals(type) && !"MODEL_LONG_CLICK".equals(type))return null;
        if(deepestActionableAtTouch(tree,candidate,type,display)!=null)
            return fields("reason","touch_masked_by_actionable_descendant");
        if(tree==null || candidate==null)return null;
        GUITreeNode root=tree.getRootNode();
        if(root==null || candidate.getRoot()!=root)return null;
        Rect clip=root.getBoundsInScreen();
        // Empty intersection stays empty; the native touch fallback to display is not applicable.
        if(clip==null || clip.isEmpty())return null;
        clip=new Rect(clip);
        if(display!=null && !clip.intersect(display))return null;
        Rect candidateBounds=clippedBounds(candidate,clip);
        if(candidateBounds==null)return null;

        Map<GUITreeNode,GUITreeNode> candidateBranches=new IdentityHashMap<GUITreeNode,GUITreeNode>();
        for(GUITreeNode child=candidate;child.getParent()!=null;child=child.getParent())
            candidateBranches.put(child.getParent(),child);
        Deque<GUITreeNode> nodes=new ArrayDeque<GUITreeNode>();nodes.add(root);
        while(!nodes.isEmpty()) {
            GUITreeNode foreground=nodes.removeFirst();
            Iterator<GUITreeNode> children=foreground.getChildren();
            while(children.hasNext())nodes.addLast(children.next());
            if(foreground==candidate || candidateBranches.containsKey(foreground)
                    || !foreground.isEnabled() || !("MODEL_LONG_CLICK".equals(type)?foreground.isLongClickable():foreground.isClickable())
                    || !Boolean.TRUE.equals(foreground.getJevVisibleToUser()))continue;
            Rect foregroundBounds=clippedBounds(foreground,clip);
            if(foregroundBounds==null || !foregroundBounds.contains(candidateBounds))continue;
            // First shared parent is the LCA. Only its direct children have comparable orders.
            for(GUITreeNode b=foreground;b.getParent()!=null;b=b.getParent()) {
                GUITreeNode a=candidateBranches.get(b.getParent());
                if(a==null)continue;
                Integer ao=a.getJevDrawingOrder(),bo=b.getJevDrawingOrder();
                if(a!=b && ao!=null && bo!=null && ao>0 && bo>ao)
                    return fields("reason","fully_occluded_by_foreground_branch","source_tree_step",tree.getTimestamp(),
                        // Object identities are the same concrete anchors used by this registry.
                        "occluded_node",Integer.toHexString(System.identityHashCode(candidate)),
                        "foreground_node",Integer.toHexString(System.identityHashCode(foreground)),
                        "occluded_bounds",candidateBounds.flattenToString(),"foreground_bounds",foregroundBounds.flattenToString(),
                        "occluded_branch_order",ao,"foreground_branch_order",bo);
                break;
            }
        }
        return null;
    }
    private static Rect clippedBounds(GUITreeNode node,Rect clip) {
        Rect bounds=node.getBoundsInScreen();
        if(bounds==null || bounds.isEmpty())return null;
        Rect clipped=new Rect(bounds);
        return clipped.intersect(clip)?clipped:null;
    }
    private static GUITreeNode deepestActionableAtTouch(GUITree tree,GUITreeNode candidate,String type,Rect display) {
        if(!"MODEL_CLICK".equals(type) && !"MODEL_LONG_CLICK".equals(type))return null;
        if(tree==null || candidate==null)return null;
        GUITreeNode root=tree.getRootNode();
        if(root==null)return null;
        Rect rootBounds=root.getBoundsInScreen();
        Rect displayBounds=display==null?rootBounds:display;
        PointF touch=ApeTouchGeometry.centerClickPoint(candidate.getBoundsInScreen(),rootBounds,displayBounds);
        return touch==null?null:deepestActionableDescendant(candidate,rootBounds,displayBounds,touch,"MODEL_LONG_CLICK".equals(type));
    }
    private static GUITreeNode deepestActionableDescendant(GUITreeNode parent,Rect rootBounds,Rect displayBounds,PointF touch,boolean longClick) {
        GUITreeNode deepest=null;
        Iterator<GUITreeNode> children=parent.getChildren();
        while(children.hasNext()) {
            GUITreeNode child=children.next();
            boolean actionable=child.isEnabled() && (longClick?child.isLongClickable():child.isClickable());
            if(actionable && ApeTouchGeometry.contains(child.getBoundsInScreen(),rootBounds,displayBounds,touch))deepest=child;
            GUITreeNode nested=deepestActionableDescendant(child,rootBounds,displayBounds,touch,longClick);
            if(nested!=null && (deepest==null || nested.getDepth()>deepest.getDepth()))deepest=nested;
        }
        return deepest;
    }
    private void revalidateStructure(Entry e,GUITree tree,GUITreeNode node,long step) {
        if(e.status==Status.REPRESENTED && e.structural && structuralRepresentation(tree,node,e.member.type)==null) {
            e.status=Status.UNSENT;e.structural=false;
            event("REPRESENTATION_INVALIDATED",e,step,merge(entryFields(e),fields("reason","touch_geometry_changed","represented_source","structure")));
        }
    }
    /** Current concrete evidence; only immutable tree geometry is cached. */
    public Status currentStatus(GUITree tree,GUITreeNode node,ModelAction action,long step) {
        GUITreeAction concrete=new GUITreeAction(tree,node,action);
        if(hardMatch(concrete)!=null)return Status.EXECUTED;
        Entry e=findAudit(concrete);
        if(e==null)return unresolvedProbe(concrete)?Status.PROBE_SUGGESTED:null;
        revalidateStructure(e,tree,node,step);
        return e.status;
    }
    /** Ambiguous pending identities supply neither new unknown navigation nor a bindable probe. */
    private boolean unresolvedProbe(GUITreeAction current) {
        for(Entry e:indexed(auditIndex,current))if(e.version==version && AFTER_SATURATION.equals(e.reviewSource) && e.status==Status.PROBE_SUGGESTED
                && e.concreteAction.getModelAction()==current.getModelAction()
                && e.sourceTree.getCurrentState()==current.getGUITree().getCurrentState()
                && ConcreteActionIdentity.canonical(e.node).equals(ConcreteActionIdentity.canonical(current.getGUITreeNode())))return true;
        ConcreteActionIdentity identity=ConcreteActionIdentity.of(current.getActionType().name(),current.getGUITreeNode());
        List<Entry> matches=identityAudits.get(identity);
        if(matches!=null)for(Entry e:matches)if(e.version==version && AFTER_SATURATION.equals(e.reviewSource) && e.status==Status.PROBE_SUGGESTED
                && e.sourceActivity.equals(activityScope(current.getGUITree())))return true;
        return false;
    }
    private Anchor anchor(GUITree tree,GUITreeNode node,String type) {
        return tree==null || node==null || type==null?null:new Anchor(tree,node,type);
    }
    /** One immutable read window only. Never reused across GUI/model mutations. */
    static final class ConcreteLookup implements AutoCloseable {
        private static final ThreadLocal<ConcreteLookup> ACTIVE=new ThreadLocal<ConcreteLookup>();
        private final ConcreteLookup previous=ACTIVE.get();
        private final IdentityHashMap<GUITreeNode,String> names=new IdentityHashMap<GUITreeNode,String>();
        private final IdentityHashMap<GUITree,Map<String,GUITreeNode>> trees=new IdentityHashMap<GUITree,Map<String,GUITreeNode>>();
        private final IdentityHashMap<GUITreeAction,String> mappings=new IdentityHashMap<GUITreeAction,String>();
        ConcreteLookup(){if(previous==null)ACTIVE.set(this);}
        static ConcreteLookup current(){return ACTIVE.get();}
        String canonical(GUITreeNode node){
            String value=names.get(node);
            if(value==null){long began=JevReviewProfile.start();value=ConcreteActionIdentity.canonical(node);names.put(node,value);
                JevReviewProfile.end("concrete_identity",began);}
            return value;
        }
        GUITreeNode find(GUITree tree,ConcreteActionIdentity identity){
            Map<String,GUITreeNode> nodes=trees.get(tree);
            if(nodes==null){
                nodes=new HashMap<String,GUITreeNode>();Deque<GUITreeNode> queue=new ArrayDeque<GUITreeNode>();queue.add(tree.getRootNode());
                while(!queue.isEmpty()){
                    JevDeadline.check();GUITreeNode node=queue.removeFirst();String name=canonical(node);
                    if(nodes.containsKey(name))nodes.put(name,null);else nodes.put(name,node);
                    Iterator<GUITreeNode> children=node.getChildren();while(children.hasNext())queue.addLast(children.next());
                }
                trees.put(tree,nodes);JevReviewProfile.add("identity_tree_index",0,1);
            }
            return nodes.get(identity.canonicalFullName());
        }
        public void close(){if(previous==null)ACTIVE.remove();}
    }
    private static ConcreteActionIdentity lookupIdentity(String type,GUITreeNode node){
        ConcreteLookup lookup=ConcreteLookup.current();
        return lookup==null?ConcreteActionIdentity.of(type,node):new ConcreteActionIdentity(type,lookup.canonical(node));
    }
    private String confirmedMappingProblem(GUITreeAction input){
        ConcreteLookup lookup=ConcreteLookup.current();
        if(lookup==null)return JevHistoryAudit.mappingProblem(input,confirmationModel.getGraph().getStates());
        if(!lookup.mappings.containsKey(input))lookup.mappings.put(input,JevHistoryAudit.mappingProblem(input,confirmationModel.getGraph().getStates()));
        return lookup.mappings.get(input);
    }
    /** Resolve one identity uniquely in a GUI tree; ambiguity never guesses. */
    private static GUITreeNode uniqueIdentityNode(GUITree tree,ConcreteActionIdentity identity) {
        if(tree==null || tree.getRootNode()==null)return null;
        ConcreteLookup lookup=ConcreteLookup.current();if(lookup!=null)return lookup.find(tree,identity);
        GUITreeNode found=null;Deque<GUITreeNode> pending=new ArrayDeque<GUITreeNode>();pending.add(tree.getRootNode());
        while(!pending.isEmpty()) {
            GUITreeNode node=pending.removeFirst();
            if(identity.equals(ConcreteActionIdentity.of(identity.actionType(),node))) {
                if(found!=null)return null;found=node;
            }
            Iterator<GUITreeNode> children=node.getChildren();while(children.hasNext())pending.addLast(children.next());
        }
        return found;
    }
    static GUITreeNode correspondingNode(GUITreeAction source,GUITree tree,ModelAction action) {
        if(source==null || tree==null || action==null || source.getGUITreeNode()==null)return null;
        GUITree original=source.getGUITree();GUITreeNode node=source.getGUITreeNode();
        if(source.getActionType()!=action.getType())return null;
        ConcreteActionIdentity identity=lookupIdentity(source.getActionType().name(),node);
        if(original==tree)return uniqueIdentityNode(tree,identity)==node?node:null;
        if(uniqueIdentityNode(original,identity)!=node)return null;
        return uniqueIdentityNode(tree,identity);
    }
    /** Audit only: count the exact existing identity relation without rebinding or relaxing it. */
    static JSONObject bindingDiagnostic(final GUITreeAction source,final GUITree tree,final ModelAction action) {
        try {return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<JSONObject>(){public JSONObject get(){
            if(source==null||source.getGUITree()==null||source.getGUITreeNode()==null||tree==null||action==null)
                return fields("reason","missing_source_tree_or_action","source_match_count",-1,"target_match_count",-1);
            ConcreteActionIdentity identity=ConcreteActionIdentity.of(source.getActionType().name(),source.getGUITreeNode());
            int original=identityCount(source.getGUITree(),identity),current=identityCount(tree,identity);
            String reason=source.getActionType()!=action.getType()?"action_type_mismatch":original!=1?"source_identity_not_unique":
                current==0?"target_node_absent":current>1?"target_node_ambiguous":"unique_identity_match";
            return fields("reason",reason,"source_match_count",original,"target_match_count",current,"concrete_identity",identity.toString());
        }});}catch(Exception unavailable){return fields("reason","diagnostic_unavailable","source_match_count",-1,"target_match_count",-1);}
    }
    private static int identityCount(GUITree tree,ConcreteActionIdentity identity) {
        if(tree==null||tree.getRootNode()==null)return 0;
        int count=0;Deque<GUITreeNode> nodes=new ArrayDeque<GUITreeNode>();nodes.add(tree.getRootNode());
        while(!nodes.isEmpty()) {GUITreeNode node=nodes.removeFirst();if(identity.equals(ConcreteActionIdentity.of(identity.actionType(),node)))count++;
            Iterator<GUITreeNode> children=node.getChildren();while(children.hasNext())nodes.addLast(children.next());}
        return count;
    }
    private boolean sameConcrete(GUITreeAction a,GUITreeAction b) {
        return a!=null && b!=null && a.getActionType()==b.getActionType()
            && a.getGUITree().getCurrentState()==b.getGUITree().getCurrentState()
            && correspondingNode(a,b.getGUITree(),b.getModelAction())==b.getGUITreeNode();
    }
    private Entry findAudit(GUITreeAction action) {
        long start=System.nanoTime();try{return findAuditNow(action);}finally{timing.record("history_match",System.nanoTime()-start);}
    }
    private Entry findAuditNow(GUITreeAction action) {
        Entry direct=audits.get(anchor(action.getGUITree(),action.getGUITreeNode(),action.getActionType().name()));
        if(direct!=null)return direct;
        for(Entry e:indexed(auditIndex,action))if(e.version==version && sameConcrete(e.concreteAction,action))return e;
        ConcreteActionIdentity identity=ConcreteActionIdentity.of(action.getActionType().name(),action.getGUITreeNode());
        List<Entry> matches=identityAudits.get(identity);
        if(matches!=null)for(Entry e:matches)if(e.sourceActivity.equals(activityScope(action.getGUITree()))
                && correspondingNode(e.concreteAction,action.getGUITree(),action.getModelAction())!=null)return e;
        return null;
    }
    private Execution hardMatch(GUITreeAction candidate) {
        long start=System.nanoTime();try{return hardMatchNow(candidate);}finally{timing.record("history_match",System.nanoTime()-start);}
    }
    private Execution hardMatchNow(GUITreeAction candidate) {
        if(candidate==null || candidate.getGUITreeNode()==null)return null;
        Execution direct=concreteExecutions.get(anchor(candidate.getGUITree(),candidate.getGUITreeNode(),candidate.getActionType().name()));
        if(direct!=null)return direct;
        State state=candidate.getGUITree().getCurrentState();
        List<Execution> matched=new ArrayList<Execution>();
        for(Execution e:indexed(executionIndex,candidate)) {
            timing.count("history_candidates",1);
            if(belongsToState(state,e.anchor.tree) && sameConcrete(candidate,e.concreteAction))matched.add(e);
        }
        if(matched.size()==1)return matched.get(0);
        // HashMap's first concrete receipt is retained when several exact historical facts match.
        if(matched.size()>1) {
            refreshExecutionOrder();
            Execution first=matched.get(0);for(Execution e:matched)if(executionOrder.get(e)<executionOrder.get(first))first=e;return first;
        }
        return null;
    }
    private void refreshExecutionOrder() {
        if(executionOrderDirty){int order=0;executionOrder.clear();for(Execution e:concreteExecutions.values())executionOrder.put(e,order++);executionOrderDirty=false;}
    }
    boolean retireIfExecuted(Entry e,long step) {
        Execution match=hardMatch(e.concreteAction);
        if(match==null)return false;
        if(e.status!=Status.EXECUTED) {
            boolean suggestion=e.status==Status.PROBE_SUGGESTED;
            e.status=Status.EXECUTED;
            if(suggestion && !e.probeClaimed && e.confidence!=null && e.confidence>localProbeConfidence){
                admit(e,step);e.naturalConfirmed=true;e.rebuildMatched|=replaying;
                lifecycle(e,step,replaying?"natural_rebuild_match":"natural_execution",replaying);
            }
            event("JEV_RESULT_STALE",e,step,merge(entryFields(e),fields("reason","hard_exact_execution",
                "transition_id",match.transitions.get(0))));
        }
        return true;
    }
    /** Combine all confirmed observations of a current sibling without duplicating it in history. */
    private Execution siblingHistory(GUITreeAction action,Member member) {
        Execution combined=null;
        for(Execution e:indexed(executionIndex,action)) {
            if(!sameConcrete(action,e.concreteAction))continue;
            if(combined==null)combined=new Execution(member,action.getGUITree().getCurrentState().getActivity(),
                anchor(action.getGUITree(),action.getGUITreeNode(),member.type),action);
            mergeExecution(combined,e);
        }
        if(combined!=null)Collections.sort(combined.transitions);
        return combined;
    }
    private void mergeExecution(Execution combined,Execution e) {
        combined.count+=e.count;combined.unsafe|=e.unsafe;combined.transitions.addAll(e.transitions);
        for(Map.Entry<String,Outcome> pair:e.outcomes.entrySet()) {
            Outcome old=pair.getValue(),out=combined.outcomes.get(pair.getKey());
            if(out==null){out=new Outcome(old.state,old.activity,old.labels,old.selfLoop,old.activityUnchanged,old.guiUnchanged,old.sourceMember,old.sourceContext,old.localChange);combined.outcomes.put(pair.getKey(),out);}
            out.count+=old.count;
        }
    }
    private boolean related(Execution e,String actionType,String sourceActivity,Member candidate) {
        if(!e.member.type.equals(actionType))return false;
        if(!e.sourceActivities.contains(sourceActivity==null?"":sourceActivity))return false;
        return !e.member.fullName.isEmpty() && new ConcreteActionIdentity(e.member.type,e.member.fullName)
            .equals(new ConcreteActionIdentity(actionType,candidate.fullName));
    }
    private boolean relatedToAny(Execution e,String actionType,String sourceActivity,List<Member> candidates) {
        for(Member candidate:candidates)if(related(e,actionType,sourceActivity,candidate))return true;
        return false;
    }
    private boolean sameStateAction(Execution e,String actionType,State state,ModelAction modelAction) {
        if(e.anchor==null || !belongsToState(state,e.anchor.tree) || !e.member.type.equals(actionType))return false;
        return modelAction!=null && e.concreteAction!=null && e.concreteAction.getModelAction()==modelAction;
    }
    private boolean belongsToState(State state,GUITree tree) {
        if(state==null || tree==null)return false;
        for(GUITree candidate:state.getGUITrees())if(candidate==tree)return true;
        return false;
    }
    private JSONArray history(List<GUITreeAction> currentActions,String actionType,String sourceActivity,State state,ModelAction modelAction,List<Member> candidates,List<Execution> selected,JSONObject contexts) throws JSONException {
        List<Execution> sameStateAction=new ArrayList<Execution>(), relatedOther=new ArrayList<Execution>();
        Set<Execution> relevant=Collections.newSetFromMap(new IdentityHashMap<Execution,Boolean>());
        if(!currentActions.isEmpty())relevant.addAll(indexed(executionIndex,currentActions.get(0)));
        for(Member member:candidates) {
            List<Execution> related=relatedIndex.get(relatedKey(sourceActivity==null?"":sourceActivity,
                new ConcreteActionIdentity(actionType,member.fullName)));
            if(related!=null)relevant.addAll(related);
        }
        for(Execution e:relevant) {
            boolean current=false;
            for(GUITreeAction action:currentActions)if(sameConcrete(action,e.concreteAction)){current=true;break;}
            if(current)continue;
            if(sameStateAction(e,actionType,state,modelAction))sameStateAction.add(e);
            else if(belongsToState(state,e.anchor.tree))continue;
            else if(relatedToAny(e,actionType,sourceActivity,candidates))relatedOther.add(e);
        }
        Comparator<Execution> order=new Comparator<Execution>() {public int compare(Execution a,Execution b) {
            String x=a.transitions.isEmpty()?"":a.transitions.get(0),y=b.transitions.isEmpty()?"":b.transitions.get(0);
            int compared=x.compareTo(y);if(compared!=0)return compared;
            refreshExecutionOrder();return Integer.compare(executionOrder.get(a),executionOrder.get(b));
        }};
        Collections.sort(sameStateAction,order);Collections.sort(relatedOther,order);
        sameStateAction.addAll(relatedOther);
        List<Execution> remaining=new ArrayList<Execution>();
        for(Execution e:sameStateAction) {
            Execution combined=null;
            for(Execution existing:remaining)if(existing.anchor.tree.getCurrentState()==e.anchor.tree.getCurrentState()
                    && existing.concreteAction.getModelAction()==e.concreteAction.getModelAction()
                    && existing.member.fullName.equals(e.member.fullName)){combined=existing;break;}
            if(combined==null) {
                combined=new Execution(e.member,e.anchor.tree.getCurrentState().getActivity(),e.anchor,e.concreteAction);
                remaining.add(combined);
            }
            mergeExecution(combined,e);
        }
        List<Execution> preferred=new ArrayList<Execution>();
        for(Member candidate:candidates) {
            boolean matched=false;
            for(Execution e:preferred)if(related(e,actionType,sourceActivity,candidate)){matched=true;break;}
            if(matched)continue;
            for(Execution e:remaining) {
                if(!belongsToState(state,e.anchor.tree) && related(e,actionType,sourceActivity,candidate)) {
                    preferred.add(e);break;
                }
            }
            if(preferred.size()==8)break;
        }
        Set<String> covered=new HashSet<String>();JSONArray out=new JSONArray();
        Map<String,String> contextIds=new LinkedHashMap<String,String>();
        while(!remaining.isEmpty() && selected.size()<8) {
            // v4 sends only concrete control evidence. Keep the deterministic
            // related-first order and transition order; post-execution outcome
            // novelty no longer selects what the model is shown.
            Execution best=preferred.isEmpty()?remaining.get(0):preferred.remove(0);
            remaining.remove(best);selected.add(best);
            JSONObject context=JevEvidence.sourceContext(best.anchor.tree);
            // All contexts use the same field construction order; compare the complete serialized value.
            String key=context.toString(),contextId=contextIds.get(key);
            if(contextId==null) {
                contextId="p"+(contexts.length()+1);contextIds.put(key,contextId);contexts.put(contextId,context);
            }
            out.put(best.member.historyView("member_id","h"+selected.size()).put("source_context_id",contextId).put("execution_count",best.count)
                .put("outcomes",outcomes(best,covered,contexts,contextIds)).put("outcomes_truncated",best.outcomes.size()>4));
        }return out;
    }
    private String contextId(String context,JSONObject contexts,Map<String,String> ids)throws JSONException {
        String id=ids.get(context);if(id!=null)return id;
        id="p"+(contexts.length()+1);ids.put(context,id);contexts.put(id,new JSONObject(context));return id;
    }
    private JSONArray outcomes(Execution e,Set<String> covered,JSONObject contexts,Map<String,String> contextIds)throws JSONException {
        JSONArray out=new JSONArray();List<String> selected=new ArrayList<String>();
        for(String outcome:e.outcomes.keySet())if(!covered.contains(outcome) && selected.size()<4)selected.add(outcome);
        for(String outcome:e.outcomes.keySet())if(!selected.contains(outcome) && selected.size()<4)selected.add(outcome);
        for(String outcome:selected){Outcome value=e.outcomes.get(outcome);out.put(value.view(contextId(value.sourceContext,contexts,contextIds)));covered.add(outcome);}return out;
    }
    public Batch page(List<Batch> groups,int maxBytes,String model) throws JSONException {
        setSemanticReviewModel(model);
        List<Entry> entries=new ArrayList<Entry>();
        List<Execution> history=new ArrayList<Execution>();boolean unsafe=false;
        for(Batch b:groups){entries.addAll(b.entries);history.addAll(b.history);unsafe|=b.unsafe;}
        String id=nextRequestId();
        State state=entries.get(0).concreteAction.getModelAction().getState();
        GUITree tree=entries.get(0).sourceTree;
        JSONObject page=new JSONObject().put("state_id",state.getGraphId()).put("activity",state.getActivity());
        JSONArray modelActions=new JSONArray(),controls=new JSONArray();
        Map<String,String> candidateIds=new LinkedHashMap<String,String>();int candidate=0;
        for(Entry e:entries)candidateIds.put(e.action+"|"+ConcreteActionIdentity.canonical(e.node),"c"+(++candidate));
        Map<String,Entry> entriesByCandidate=new LinkedHashMap<String,Entry>();
        for(Entry e:entries)entriesByCandidate.put(candidateIds.get(e.action+"|"+ConcreteActionIdentity.canonical(e.node)),e);
        for(ModelAction action:state.getActions()) {
            if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled()||action.getResolvedGUITree()!=tree)continue;
            GUITreeNode[] nodes=action.getResolvedNodes();if(nodes==null||nodes.length==0)continue;
            JSONArray executed=new JSONArray(),unexecuted=new JSONArray();
            for(GUITreeNode node:nodes) {
                JSONObject control=JevSemanticReview.node(node,action.getType().name());
                String key=action.getGraphId()+"|"+ConcreteActionIdentity.canonical(node);
                if(candidateIds.containsKey(key)) {
                    control.put("candidate_id",candidateIds.get(key));unexecuted.put(control);
                } else if(hardMatch(new GUITreeAction(tree,node,action))!=null)executed.put(control);
                else unexecuted.put(control);
                control.put("model_action_id",action.getGraphId());controls.put(control);
            }
            modelActions.put(new JSONObject().put("model_action_id",action.getGraphId()).put("action_type",action.getType().name())
                .put("executed_concrete_actions",executed).put("unexecuted_concrete_actions",unexecuted));
        }
        List<android.graphics.Rect> pageBounds=new ArrayList<android.graphics.Rect>();
        for(int i=0;i<controls.length();i++) {
            JSONArray b=controls.getJSONObject(i).getJSONArray("bounds");
            pageBounds.add(new android.graphics.Rect(b.getInt(0),b.getInt(1),b.getInt(2),b.getInt(3)));
        }
        // Keep the same pairwise spatial evidence as asynchronous review.
        for(int i=0;i<controls.length();i++) {
            JSONObject control=controls.getJSONObject(i);JSONArray relations=new JSONArray();
            android.graphics.Rect from=pageBounds.get(i);
            for(int j=0;j<pageBounds.size();j++)if(i!=j) {
                android.graphics.Rect to=pageBounds.get(j);String relation;
                if(from.right<=to.left)relation="left_of";else if(from.left>=to.right)relation="right_of";
                else if(from.bottom<=to.top)relation="above";else if(from.top>=to.bottom)relation="below";else relation="overlaps";
                relations.put(new JSONObject().put("relation",relation).put("control_index",j));
            }
            control.put("spatial_relations",relations);
        }
        JSONArray selectedIds=new JSONArray();for(String value:candidateIds.values())selectedIds.put(value);
        page.put("model_actions",modelActions).put("page_controls",controls).put("candidate_ids",selectedIds);
        page.put("input_signature",JevEvidence.digest(page.toString()));
        JevProtocol.Request request=JevProtocol.semantic(id,model,page,maxBytes);
        Batch pageBatch=new Batch(version,id,groups.get(0).state,null,entries,request.stateJson,history,Collections.<String,Execution>emptyMap(),unsafe);
        pageBatch.request=request;pageBatch.semanticEntries=Collections.unmodifiableMap(entriesByCandidate);pageBatch.pageGroups=Collections.unmodifiableList(new ArrayList<Batch>(groups));
        for(Entry e:entries) {
            e.requestId=id;
            ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
            reviewLedger.bind(syncReviewContract,e.sourceActivity,identity,JevEvidence.guiIdentity(e.sourceTree));
        }
        return pageBatch;
    }
    /** Bind a completed decision to the current GUI only after unique identity matching. */
    public void bindCurrent(Batch b,GUITree currentTree) {
        if(b==null || currentTree==null)return;
        for(Entry e:b.entries) {
            ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
            if(e.sourceActivity.equals(activityScope(currentTree)) && uniqueIdentityNode(currentTree,identity)!=null)
                reviewLedger.bind(syncReviewContract,e.sourceActivity,identity,JevEvidence.guiIdentity(currentTree));
        }
    }
    public JSONObject receipt(Batch b) throws JSONException {
        JSONArray candidates=new JSONArray(),history=new JSONArray(),groups=new JSONArray();
        for(Batch group:b.pageGroups)groups.put(receipt(group));
        for(int i=0;i<b.entries.size();i++){Entry e=b.entries.get(i);candidates.put(fields("candidate_id","c"+(i+1),"entry_id",e.id,"review_source",b.reviewSource));}
        for(int i=0;i<b.history.size();i++){Execution e=b.history.get(i);
            JSONArray transitions=new JSONArray();for(String t:e.transitions)transitions.put(t);
            history.put(fields("member_id","h"+(i+1),"transition_ids",transitions));}
        JSONArray siblings=new JSONArray();
        for(Map.Entry<String,Execution> pair:b.executedSiblings.entrySet()) {
            Execution e=pair.getValue();JSONArray transitions=new JSONArray();for(String t:e.transitions)transitions.put(t);
            siblings.put(fields("sibling_id",pair.getKey(),"transition_ids",transitions));
        }
        JSONObject receipt=fields("request_id",b.id,"abstraction_version",b.version,"state_id",b.state,"model_action_id",b.action,
            "sampling_policy",SAMPLING,"groups",groups,"candidates",candidates,"current_siblings",siblings,"executed_history_other_pages",history);
        if(b.request!=null)receipt.put("local_target_states",new JSONObject(b.request.localTargetStatesJson));
        return receipt;
    }
    public JSONObject requestFields(Batch b) {
        return fields("request_id",b.id,"review_source",b.reviewSource,"candidate_count",b.entries.size(),"history_count",b.history.size()+b.executedSiblings.size(),
            "request_bytes",b.request==null?JSONObject.NULL:b.request.requestBytes,
            "evidence_digest",b.request==null?JevEvidence.digest(b.stateJson):b.request.evidenceDigest,"sampling_policy",SAMPLING);
    }
    /** Apply a complete request-fingerprint hit without another provider call. */
    public boolean reuseFingerprint(Batch b,long step) {
        if(b==null || b.request==null)return false;
        String scope=b.entries.isEmpty()?"":b.entries.get(0).sourceActivity;
        Map<ConcreteActionIdentity,JevReviewLedger.Decision> hits=reviewLedger.byFingerprint(syncReviewContract,scope,b.request.inputFingerprint);
        if(hits.isEmpty())return false;
        for(Entry e:b.entries) {
            ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
            if(!hits.containsKey(identity))return false;
        }
        for(Entry e:b.entries) {
            JevReviewLedger.Decision decision=hits.get(ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node));
            e.requestId=b.id;e.confidence=decision.confidence;e.decisionStep=step;e.decisionNanos=System.nanoTime();
            if("PROBE".equals(decision.outcome))e.status=Status.PROBE_SUGGESTED;
            else if("EXECUTED".equals(decision.outcome))e.status=Status.EXECUTED;
            else e.status=Status.REPRESENTED;
            event("JEV_RESULT_REUSED",e,step,merge(entryFields(e),fields("request_id",b.id,"reason","input_fingerprint","input_fingerprint",b.request.inputFingerprint,"choice",decision.outcome,"confidence",decision.confidence)));
        }
        return true;
    }
    public void requestEvent(String name,Batch b,long step,JSONObject extra) {events.emit(name,step,b.version,b.state,b.action,merge(requestFields(b),extra));}

    /** Emit an asynchronous semantic decision with the same run-local identity
     * fields used by the synchronous audit path.  This is evidence of a real
     * parsed provider result, not a derived counter. */
    public void semanticEvent(String name,long step,String state,String action,JSONObject fields) {
        events.emit(name,step,version,state,action,fields);
    }
    public void apply(Batch b,String raw,long step) throws JSONException {
        long start=System.nanoTime();try{applyNow(b,raw,step);}finally{timing.record("audit_apply",System.nanoTime()-start);}
    }
    private void applyNow(Batch b,String raw,long step) throws JSONException {
        if(b.completed)return;
        List<String> ids=new ArrayList<String>();for(int i=0;i<b.entries.size();i++)ids.add("c"+(i+1));
        JevProtocol.Result result=b.request!=null&&b.request.semantic?JevProtocol.fromProvider(raw,b.request):JevProtocol.validate(raw,ids); // Validate before first mutation.
        b.completed=true;
        for(int i=0;i<b.entries.size();i++) {
            Entry e=b.request!=null&&b.request.semantic?b.semanticEntries.get("c"+(i+1)):b.entries.get(i);
            if(e==null)throw new JSONException("missing semantic candidate binding");
            JevProtocol.Decision d=result.decisions.get("c"+(i+1));
            boolean current=b.id.equals(e.requestId) && e.status==Status.QUEUED;
            if(retireIfExecuted(e,step) || !current) {
                event("JEV_RESULT_STALE",e,step,merge(entryFields(e),fields("request_id",b.id,"review_source",b.reviewSource,"reason","execution_or_superseded_review")));continue;
            }
            e.decisionStep=step;e.decisionNanos=System.nanoTime();e.confidence=d.confidence;
            JSONObject f=merge(entryFields(e),fields("request_id",b.id,"review_source",b.reviewSource,"p_probe",d.pProbe,"p_represented",d.pRepresented,"confidence",d.confidence==null?JSONObject.NULL:d.confidence,"choice",d.decision));
            String cachedOutcome;
            if(d.confidence==null || d.confidence<=localProbeConfidence) {
                e.status=Status.RETIRED;
                cachedOutcome="REVIEW_FAILED";
                event("AUDIT_RETIRED",e,step,merge(f,fields("reason",d.confidence==null?"confidence_missing":"low_confidence","retired",true)));
            } else if(b.request!=null&&b.request.semantic?JevSemanticProtocol.PROMOTE.equals(d.representativeModelActionId):"PROBE".equals(d.decision)) {
                e.status=Status.PROBE_SUGGESTED;event("DECISION_PROBE",e,step,f);
                event("PROBE_SUGGESTED",e,step,entryFields(e));
                cachedOutcome="PROBE";
            }
            else {e.status=Status.REPRESENTED;event("DECISION_REPRESENTED",e,step,merge(f,fields("represented_source","semantic","representative_model_action_id",d.representativeModelActionId==null?JSONObject.NULL:d.representativeModelActionId)));cachedOutcome="REPRESENTED";}
            // Failed/low-confidence reviews remain retryable; only a valid
            // PROBE or REPRESENTED conclusion enters the reusable ledger.
            if(!"REVIEW_FAILED".equals(cachedOutcome))
                reviewLedger.put(syncReviewContract,e.sourceActivity,ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node),
                    cachedOutcome,d.confidence,d.representativeModelActionId,b.request==null?null:b.request.inputFingerprint);
        }
    }
    public void selected(Entry e,long step) {
        admit(e,step);
        event("PROBE_SELECTED",e,step,merge(entryFields(e),fields("decision_step",e.decisionStep,"decision_monotonic_ns",e.decisionNanos)));}
    private void admit(Entry e,long step) {
        if(e.additionalAdmitted)return;
        e.additionalAdmitted=true;admittedProbes.put(e.id,e);lifecycle(e,step,"admitted",false);
    }
    public void failed(Entry e,long step,String reason) {
        if(e.status!=Status.PROBE_SUGGESTED)return;
        e.probeClaimed=false;
        event("PROBE_EXECUTION_FAILED",e,step,merge(entryFields(e),fields("reason",reason,"retired",false)));
    }
    public void unsent(Batch b,long step) {
        unsentDeferrals++;
        for(Entry e:b.entries) if(b.id.equals(e.requestId) && e.status==Status.QUEUED) {
            e.status=Status.UNSENT;
            // Emit only the first transition to unsent, not on every busy safe point.
            if(!e.unsentLogged){event("AUDIT_UNSENT",e,step,entryFields(e));e.unsentLogged=true;}
            e.requestId=null;
        }
    }
    public void admitted(Batch b,long step) {
        for(Entry e:b.entries)if(b.id.equals(e.requestId) && e.status==Status.QUEUED) {
            event("AUDIT_ACCEPTED",e,step,entryFields(e));
        }
    }
    public void deliveryFailed(Batch b,long step,String reason) {
        if(b.completed)return;
        b.completed=true;
        for(Entry e:b.entries) if((e.requestId==null || b.id.equals(e.requestId)) && (e.status==Status.QUEUED || e.status==Status.UNSENT)) {
            e.status=Status.DELIVERY_FAILED;event("AUDIT_DELIVERY_FAILED",e,step,merge(entryFields(e),fields("reason",reason)));
        }
    }
    private void lifecycle(Entry e,long step,String terminal,boolean rebuild) {
        events.emit("PROBE_LIFECYCLE",step,e.version,e.state,e.action,merge(entryFields(e),fields("terminal",terminal,
            "admitted",e.additionalAdmitted,"natural_execution",e.naturalConfirmed,"rebuild_match",rebuild)));
    }
    void finishAccounting(long step) {
        int batch=0;
        for(Entry e:admittedProbes.values()) {
            String terminal=e.probeConfirmed?"probe_executed":e.naturalConfirmed?"natural_execution":
                e.terminalReason!=null?e.terminalReason:probePending(e)?"unclassified":
                e.status==Status.RETIRED || e.status==Status.DELIVERY_FAILED?"failed_or_interrupted":"unclassified";
            events.emit("PROBE_ACCOUNTING_FINAL",step,e.version,e.state,e.action,merge(entryFields(e),fields("terminal",terminal,
                "admitted",true,"rebuild_match",e.rebuildMatched,"reason",e.terminalReason==null?e.status.name():e.terminalReason)));
            if(++batch%32==0)events.flush();
        }
    }
    public void counters(long step) {
        events.emit("JEV_COUNTERS",step,version,null,null,fields("dedup_count",dedupCount,"unsent_deferrals",unsentDeferrals));
    }
    public Batch slice(Batch source,int from,int to) throws JSONException {
        String id=nextRequestId();List<Entry> entries=new ArrayList<Entry>(source.entries.subList(from,to));
        JSONArray candidates=new JSONArray();
        for(int i=0;i<entries.size();i++){Entry e=entries.get(i);e.requestId=id;candidates.put(e.member.view("candidate_id","c"+(i+1)));}
        JSONObject state=new JSONObject(source.stateJson);state.put("candidates",candidates);
        return new Batch(source.version,id,source.state,source.action,entries,state.toString(),source.history,source.executedSiblings,source.unsafe);
    }
    public void executed(String s,String a,Member member,GUITreeAction concreteAction,String sourceActivity,String targetState,String targetActivity,JSONArray labels,String transition,
            long step,boolean newState,boolean newAction,boolean refinement,boolean selectedProbe,boolean replay,boolean unsafeResult,Boolean guiUnchanged) {
        executed(s,a,member,concreteAction,null,sourceActivity,targetState,targetActivity,labels,transition,step,newState,newAction,refinement,selectedProbe,replay,unsafeResult,guiUnchanged);
    }
    public void executed(String s,String a,Member member,GUITreeAction concreteAction,GUITree targetTree,String sourceActivity,String targetState,String targetActivity,JSONArray labels,String transition,
            long step,boolean newState,boolean newAction,boolean refinement,boolean selectedProbe,boolean replay,boolean unsafeResult,Boolean guiUnchanged) {
        Anchor anchor=concreteAction==null?null:anchor(concreteAction.getGUITree(),concreteAction.getGUITreeNode(),member.type);
        if(anchor==null)throw new IllegalArgumentException("confirmed execution requires concrete APE anchor");
        boolean newActivity=false;
        Execution x=concreteExecutions.get(anchor);if(x==null){x=new Execution(member,sourceActivity,anchor,concreteAction);concreteExecutions.put(anchor,x);executionOrderDirty=true;addToIndex(executionIndex,concreteAction,x);newActivity=true;}else newActivity=x.sourceActivities.add(sourceActivity==null?"":sourceActivity);
        if(newActivity) {
            String key=relatedKey(sourceActivity==null?"":sourceActivity,
                concreteAction.getGUITreeNode()==null?new ConcreteActionIdentity(member.type,member.fullName):ConcreteActionIdentity.of(member.type,concreteAction.getGUITreeNode()));
            List<Execution> values=relatedIndex.get(key);if(values==null){values=new ArrayList<Execution>();relatedIndex.put(key,values);}values.add(x);
        }
        changed(concreteAction.getModelAction());
        x.count++;x.unsafe|=unsafeResult;
        reviewLedger.markExecutedMatching(null,concreteAction.getGUITree().getCurrentState(),
            concreteAction.getGUITreeNode()==null?new ConcreteActionIdentity(member.type,member.fullName):ConcreteActionIdentity.of(member.type,concreteAction.getGUITreeNode()));
        boolean selfLoop=s.equals(targetState),activityUnchanged=Objects.equals(sourceActivity,targetActivity);
        x.transitions.add(transition);
        String sourceContext=JevEvidence.sourceContext(anchor.tree).toString();
        String localChange=localChange(concreteAction.getGUITreeNode(),targetTree);
        String ok=new JSONArray().put(sourceContext).put(targetActivity).put(targetState).put(labels).put(selfLoop).put(activityUnchanged).put(guiUnchanged).put(localChange).toString();
        Outcome out=x.outcomes.get(ok);if(out==null){out=new Outcome(targetState,targetActivity,labels.toString(),selfLoop,activityUnchanged,guiUnchanged,member,sourceContext,localChange);x.outcomes.put(ok,out);}out.count++;
        Entry e=audits.get(anchor);
        replaying=replay;
        try{for(Entry audited:indexed(auditIndex,concreteAction))if(sameConcrete(audited.concreteAction,concreteAction))retireIfExecuted(audited,step);}
        finally{replaying=false;}
        if(!replay) {
            JSONObject f=fields("transition_id",transition,"new_state",newState,"new_model_action",newAction,"refinement",refinement,
                "self_loop",selfLoop,"activity_unchanged",activityUnchanged,"gui_unchanged",guiUnchanged,
                "target_state",targetState,"input_confirmed",true);
            if(e!=null)merge(f,entryFields(e));
            events.emit("NATIVE_TRANSITION",step,version,s,a,f);
            if(selectedProbe && e!=null)event("PROBE_EXECUTED",e,step,merge(f,fields("decision_step",e.decisionStep,"decision_monotonic_ns",e.decisionNanos)));
        }
    }
    private static String localChange(GUITreeNode source,GUITree target) {
        if(source==null || target==null)return null;
        List<GUITreeNode> matches=new ArrayList<GUITreeNode>();collectSameName(target.getRootNode(),JevIdentity.fullName(source).toXPath(),matches);
        if(matches.size()!=1)return null;
        GUITreeNode after=matches.get(0);JSONObject change=new JSONObject();
        try {
            if(source.isCheckable() && after.isCheckable() && source.isChecked()!=after.isChecked())
                change.put("checked",new JSONObject().put("before",source.isChecked()).put("after",after.isChecked()));
            Boolean before=source.getJevSelected(),afterSelected=after.getJevSelected();
            if(before!=null && afterSelected!=null && !before.equals(afterSelected))
                change.put("selected",new JSONObject().put("before",before).put("after",afterSelected));
            return change.length()==0?null:change.toString();
        } catch(JSONException e){return null;}
    }
    private static void collectSameName(GUITreeNode node,String name,List<GUITreeNode> out) {
        if(node==null || out.size()>1)return;
        if(JevIdentity.fullName(node).toXPath().equals(name))out.add(node);
        Iterator<GUITreeNode> it=node.getChildren();while(it.hasNext())collectSameName(it.next(),name,out);
    }
}
