JevRegistry.java

原始文件

1package com.android.commands.monkey.ape.jev;
2
3import java.util.*;
4import org.json.*;
5import android.graphics.PointF;
6import android.graphics.Rect;
7import com.android.commands.monkey.ape.events.ApeTouchGeometry;
8import com.android.commands.monkey.ape.tree.GUITree;
9import com.android.commands.monkey.ape.tree.GUITreeAction;
10import com.android.commands.monkey.ape.tree.GUITreeNode;
11import com.android.commands.monkey.ape.model.State;
12import com.android.commands.monkey.ape.model.ModelAction;
13import static com.android.commands.monkey.ape.jev.JevProtocol.AFTER_SATURATION;
14
15/** Main-thread-only derived bookkeeping. Raw native transitions remain owned by APE. */
16public final class JevRegistry {
17    public enum Status { UNSENT, WAITING_HISTORY, QUEUED, REPRESENTED, PROBE_SUGGESTED, RETIRED, DELIVERY_FAILED, EXECUTED }
18    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";
19    /** Evidence fields; stable concrete identity is supplied by ConcreteActionIdentity. */
20    public static final class Member {
21        public final String type, labelsJson, roleSignatureJson, localContextJson;
22        final String fullName;
23        public final boolean unsafe;
24        public Member(String type,JSONArray labels,JSONArray roleSignature,String fullName,boolean unsafe) {
25            this(type,labels,roleSignature,fullName,unsafe,null);
26        }
27        public Member(String type,JSONArray labels,JSONArray roleSignature,String fullName,boolean unsafe,JSONObject localContext) {
28            this.type=type;labelsJson=labels.toString();roleSignatureJson=roleSignature.toString();
29            localContextJson=localContext==null?null:localContext.toString();this.fullName=fullName;this.unsafe=unsafe;
30        }
31        JSONObject view(String idField,String id) throws JSONException {
32            JSONObject out=new JSONObject().put(idField,id).put("target_subtree_labels",new JSONArray(labelsJson))
33                .put("role_signature",new JSONArray(roleSignatureJson));
34            out.put("local_context",localContextJson==null?JSONObject.NULL:new JSONObject(localContextJson));
35            return out;
36        }
37        JSONObject historyView(String idField,String id) throws JSONException {
38            return new JSONObject().put(idField,id);
39        }
40    }
41    public static final class Observation {
42        public final String state,action,type,sourceActivity;
43        public final GUITree sourceTree;
44        public final GUITreeAction concreteAction;
45        public final List<GUITreeNode> concreteNodes;
46        public final boolean valid,saturated;
47        public final List<Member> members;
48        public Observation(String state,String action,String type,String sourceActivity,GUITree sourceTree,GUITreeAction concreteAction,
49                List<GUITreeNode> concreteNodes,boolean valid,boolean saturated,List<Member> members) {
50            this.state=state;this.action=action;this.type=type;this.sourceActivity=sourceActivity==null?"":sourceActivity;
51            this.sourceTree=sourceTree;this.concreteAction=concreteAction;
52            this.concreteNodes=Collections.unmodifiableList(new ArrayList<GUITreeNode>(concreteNodes));
53            this.valid=valid;this.saturated=saturated;this.members=members;
54        }
55    }
56    public static final class Entry {
57        public long version;
58        public final String id;
59        public String state,action;
60        public final String sourceActivity;
61        public final String reviewSource=AFTER_SATURATION;
62        public boolean structural;
63        boolean additionalAdmitted,probeConfirmed,naturalConfirmed,rebuildMatched,probeClaimed;
64        String terminalReason;
65        public final GUITree sourceTree;
66        public final GUITreeNode node;
67        public final GUITreeAction concreteAction;
68        public Member member;
69        public Status status=Status.QUEUED;
70        public String requestId;
71        public Double confidence;
72        public long decisionStep,decisionNanos;
73        boolean unsentLogged;
74        Entry(long v,String id,String s,String a,GUITree sourceTree,GUITreeAction concreteAction,Member m,String activity) {
75            version=v;this.id=id;state=s;action=a;sourceActivity=activity==null?"":activity;
76            this.sourceTree=sourceTree;this.concreteAction=concreteAction;this.node=concreteAction==null?null:concreteAction.getGUITreeNode();
77            member=m;
78        }
79    }
80    public static final class Batch {
81        public final long version;
82        public final String id,state,action,stateJson,reviewSource;
83        public final List<Entry> entries;
84        public final List<Execution> history;
85        public final Map<String,Execution> executedSiblings;
86        public final boolean unsafe;
87        public JevProtocol.Request request;
88        Map<String,Entry> semanticEntries=Collections.emptyMap();
89        List<Batch> pageGroups=Collections.emptyList();
90        private boolean completed;
91        Batch(long v,String id,String s,String a,List<Entry> es,String json,List<Execution> history,Map<String,Execution> siblingKeys,boolean unsafe) {
92            version=v;this.id=id;state=s;action=a;entries=Collections.unmodifiableList(new ArrayList<Entry>(es));
93            stateJson=json;this.history=Collections.unmodifiableList(new ArrayList<Execution>(history));this.unsafe=unsafe;
94            reviewSource=es.get(0).reviewSource;
95            executedSiblings=Collections.unmodifiableMap(new LinkedHashMap<String,Execution>(siblingKeys));
96        }
97    }
98    static final class Outcome {
99        final String state,activity,labels,sourceContext,localChange; final Member sourceMember; int count;
100        final boolean selfLoop,activityUnchanged;final Boolean guiUnchanged;
101        Outcome(String s,String a,String l,boolean loop,boolean sameActivity,Boolean sameGui,Member source,String context,String change) {
102            state=s;activity=a;labels=l;selfLoop=loop;activityUnchanged=sameActivity;guiUnchanged=sameGui;sourceMember=source;sourceContext=context;localChange=change;
103        }
104        JSONObject view(String contextId) throws JSONException {return new JSONObject().put("source_snapshot",new JSONObject()
105            .put("source_context_id",contextId).put("target_subtree_labels",new JSONArray(sourceMember.labelsJson))
106            .put("role_signature",new JSONArray(sourceMember.roleSignatureJson)).put("local_context",sourceMember.localContextJson==null?JSONObject.NULL:new JSONObject(sourceMember.localContextJson)))
107            .put("target_activity",JevEvidence.boundedText(activity)).put("target_state",state)
108            .put("count",count).put("result_labels",new JSONArray(labels)).put("self_loop",selfLoop)
109            .put("activity_unchanged",activityUnchanged).put("gui_unchanged",guiUnchanged==null?JSONObject.NULL:guiUnchanged)
110            .put("local_change",localChange==null?JSONObject.NULL:new JSONObject(localChange));}
111    }
112    static final class Execution {
113        final Member member; int count; boolean unsafe;
114        final Set<String> sourceActivities=new HashSet<String>();
115        final Anchor anchor;
116        final GUITreeAction concreteAction;
117        final SortedMap<String,Outcome> outcomes=new TreeMap<String,Outcome>();
118        final List<String> transitions=new ArrayList<String>();
119        Execution(Member m,String sourceActivity,Anchor anchor,GUITreeAction concreteAction) {
120            member=m;unsafe=m.unsafe;this.anchor=anchor;this.concreteAction=concreteAction;
121            sourceActivities.add(sourceActivity==null?"":sourceActivity);
122        }
123    }
124    /** APE-owned exact execution anchor; this is native scope, not review identity. */
125    static final class Anchor {
126        final GUITree tree; final GUITreeNode node; final String type;
127        Anchor(GUITree tree,GUITreeNode node,String type) {this.tree=tree;this.node=node;this.type=type;}
128        public int hashCode() {return System.identityHashCode(tree)*31*31+System.identityHashCode(node)*31+type.hashCode();}
129        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);}
130    }
131    private long version=-1,entrySeq,requestSeq;
132    /** Read the existing per-action execution index; unrelated States do not invalidate work. */
133    long executionRevision(State state) {
134        long count=0;
135        for(ModelAction action:state.getActions()) {
136            JevDeadline.check();List<Execution> values=executionIndex.get(new ActionKey(state,action));
137            if(values!=null)for(Execution value:values){JevDeadline.check();count+=value.count;}
138        }
139        return count;
140    }
141    /** Revision for one Pair's source ModelAction; unrelated actions in the
142        same State must not invalidate an otherwise identical Pair. */
143    long executionRevision(State state,ModelAction action) {
144        long profileStart=JevReviewProfile.start();
145        try {
146        if(state==null||action==null)return 0;
147        long count=0;List<Execution> values=executionIndex.get(new ActionKey(state,action));
148        if(values!=null)for(Execution value:values){JevDeadline.check();count+=value.count;}
149        return count;
150        } finally {JevReviewProfile.end("execution_revision",profileStart);}
151    }
152    private String syncReviewContract="sync-v4";
153    public void setSemanticReviewModel(String model){syncReviewContract="semantic-v1:"+(model==null?"":model);}
154    private String requestNamespace="";
155    public void requestNamespace(String value) {
156        if(value==null || !value.matches("[A-Za-z0-9_-]+"))throw new IllegalArgumentException("Invalid branch request namespace");
157        if(requestSeq!=0)throw new IllegalStateException("Cannot rename submitted request history");
158        requestNamespace=value+"-";
159    }
160    private String nextRequestId(){return requestNamespace+"r"+(++requestSeq);}
161    /** Read-only diagnostic view of the exact index used by candidate identity matching. */
162    List<Execution> historyAuditExecutions(State state,ModelAction action) {
163        List<Execution> values=executionIndex.get(new ActionKey(state,action));
164        return values==null?Collections.<Execution>emptyList():Collections.unmodifiableList(values);
165    }
166    public JSONObject historyEvidence() {
167        int observations=0;for(Execution execution:concreteExecutions.values())observations+=execution.count;
168        return fields("concrete_executions",concreteExecutions.size(),"execution_observations",observations,
169            "request_count",requestSeq,"advice_count",audits.size());
170    }
171    public void rebuildHistory(long v,long step) {
172        if(!audits.isEmpty() || requestSeq!=0)throw new IllegalStateException("Rebuild only at unsent checkpoint");
173        concreteExecutions.clear();executionIndex.clear();relatedIndex.clear();executionOrder.clear();executionOrderDirty=false;
174        version=v;
175    }
176    private final LinkedHashMap<Anchor,Entry> audits=new LinkedHashMap<Anchor,Entry>();
177    /** Every entry is one confirmed native transition for one APE concrete anchor. */
178    private final Map<Anchor,Execution> concreteExecutions=new HashMap<Anchor,Execution>();
179    // Neither cache level keeps a tree alive through a node's DOM/document back-reference.
180    // A present null is a cached negative structural result, independent of audit status.
181    private final Map<GUITree,Map<GUITreeNode,Map<String,JSONObject>>> structures=
182        new WeakHashMap<GUITree,Map<GUITreeNode,Map<String,JSONObject>>>();
183    private static final class ActionKey {
184        final State state; final ModelAction action;
185        ActionKey(GUITreeAction concrete){state=concrete.getGUITree().getCurrentState();action=concrete.getModelAction();}
186        ActionKey(State state,ModelAction action){this.state=state;this.action=action;}
187        public int hashCode(){return System.identityHashCode(state)*31+System.identityHashCode(action);}
188        public boolean equals(Object other){if(!(other instanceof ActionKey))return false;ActionKey k=(ActionKey)other;return state==k.state && action==k.action;}
189    }
190    private final Map<ActionKey,List<Entry>> auditIndex=new HashMap<ActionKey,List<Entry>>();
191    private final Map<ConcreteActionIdentity,List<Entry>> identityAudits=new HashMap<ConcreteActionIdentity,List<Entry>>();
192    private final Map<ActionKey,List<Execution>> executionIndex=new HashMap<ActionKey,List<Execution>>();
193    private final Map<String,List<Execution>> relatedIndex=new HashMap<String,List<Execution>>();
194    private final Map<String,Entry> admittedProbes=new LinkedHashMap<String,Entry>();
195    private boolean replaying,executionOrderDirty;
196    private final Map<Execution,Integer> executionOrder=new IdentityHashMap<Execution,Integer>();
197    private final Map<String,Entry> entriesById=new HashMap<String,Entry>();
198    private final JevReviewLedger reviewLedger=new JevReviewLedger();
199    interface ChangeListener {void changed(ModelAction action);}
200    private ChangeListener changes;
201    private JevTiming timing=new JevTiming();
202    void observeChanges(ChangeListener listener,JevTiming timing){changes=listener;this.timing=timing;}
203    private void changed(ModelAction action){if(changes!=null)changes.changed(action);}
204    private static <T> void addToIndex(Map<ActionKey,List<T>> index,GUITreeAction action,T value) {
205        ActionKey key=new ActionKey(action);List<T> values=index.get(key);
206        if(values==null){values=new ArrayList<T>();index.put(key,values);}values.add(value);
207    }
208    private static <T> List<T> indexed(Map<ActionKey,List<T>> index,GUITreeAction action) {
209        List<T> values=index.get(new ActionKey(action));return values==null?Collections.<T>emptyList():values;
210    }
211    List<Entry> actionEntries(ModelAction action,GUITree tree){
212        List<Entry> out=new ArrayList<Entry>(indexed(auditIndex,new GUITreeAction(tree,null,action)));
213        GUITreeNode[] nodes=action==null?null:action.getResolvedNodes();
214        if(nodes!=null)for(GUITreeNode node:nodes) {
215            ConcreteActionIdentity identity=ConcreteActionIdentity.of(action.getType().name(),node);List<Entry> matches=identityAudits.get(identity);
216            if(matches==null)continue;
217            for(Entry entry:matches)if(entry.sourceActivity.equals(activityScope(tree))&&!out.contains(entry))out.add(entry);
218        }
219        return out;
220    }
221    Entry entry(String id){return entriesById.get(id);}
222    public List<Entry> pendingEntries(ModelAction action,GUITree tree) {
223        List<Entry> out=new ArrayList<Entry>();for(Entry e:actionEntries(action,tree))if(probeAvailable(e))out.add(e);return out;
224    }
225    private final Set<String> seenSaturation=new HashSet<String>(),historyDeferredLogged=new HashSet<String>();
226    private final JevEvents events;
227    private final double localProbeConfidence;
228    public static final double NAVIGATION_CONFIDENCE=0.8;
229    private long dedupCount,unsentDeferrals;
230    public JevRegistry(JevEvents events,double threshold) {
231        if(Double.isNaN(threshold) || threshold<0 || threshold>1)throw new IllegalArgumentException("invalid confidence threshold");
232        this.events=events;localProbeConfidence=threshold;
233    }
234    static String group(long v,String s,String a) {return new JSONArray().put(v).put(s).put(a).toString();}
235    private static String activityScope(GUITree tree) {if(tree==null)return "";String value=tree.getActivityClassName();return value==null?"":value;}
236    public long version() {return version;}
237    public JevReviewLedger reviewLedger(){return reviewLedger;}
238    public static JSONObject fields(Object... pairs) {
239        JSONObject j=new JSONObject();
240        try {for(int i=0;i<pairs.length;i+=2) j.put((String)pairs[i],pairs[i+1]);}
241        catch(JSONException e) {throw new IllegalStateException(e);} return j;
242    }
243    private JSONObject entryFields(Entry e) {return fields("entry_id",e.id,"request_id",e.requestId==null?JSONObject.NULL:e.requestId,"review_source",e.reviewSource);}
244    private void event(String name,Entry e,long step,JSONObject f) {
245        changed(e.concreteAction.getModelAction());events.emit(name,step,e.version,e.state,e.action,f);
246        if(e.additionalAdmitted) {
247            if("DECISION_REPRESENTED".equals(name)) {e.terminalReason="failed_or_interrupted";lifecycle(e,step,"superseded_by_representation",false);}
248            if("PROBE_EXECUTION_FAILED".equals(name) || "anchor_unresolved_after_refinement".equals(f.optString("reason")))e.terminalReason="failed_or_interrupted";
249        }
250    }
251    public void reset(long v,long step) {
252        if(v==version)return;
253        // APE refinement replaces graph objects. Review decisions and confirmed
254        // review facts belong to the run-level ledger; native execution facts
255        // are rebuilt from APE's transition history by JevRuntime.
256        version=v;
257        for(Entry e:audits.values())e.version=v;
258        concreteExecutions.clear();executionIndex.clear();relatedIndex.clear();
259        executionOrder.clear();executionOrderDirty=false;
260        seenSaturation.clear();historyDeferredLogged.clear();
261    }
262
263    /** End a selection round while retaining reusable review decisions. */
264    public void discardAdvice(Entry executing,long step) {
265        for(Entry e:audits.values())if(e!=executing && e.status==Status.PROBE_SUGGESTED)e.probeClaimed=false;
266    }
267
268    public List<Entry> suggestions(State state,GUITree tree) {
269        List<Entry> out=new ArrayList<Entry>();
270        for(ModelAction action:state.getActions())for(Entry e:actionEntries(action,tree))
271            if(probeAvailable(e) && JevEvidence.guiIdentity(e.sourceTree).equals(JevEvidence.guiIdentity(tree))
272                && correspondingNode(e.concreteAction,tree,action)!=null)out.add(e);
273        Collections.sort(out,new Comparator<Entry>(){public int compare(Entry a,Entry b){
274            int c=Double.compare(b.confidence,a.confidence);return c!=0?c:a.id.compareTo(b.id);
275        }});
276        List<Entry> unique=new ArrayList<Entry>();
277        for(Entry e:out){boolean duplicate=false;for(Entry old:unique)if(sameConcrete(e.concreteAction,old.concreteAction)){duplicate=true;break;}
278            if(!duplicate)unique.add(e);}
279        return unique;
280    }
281    static JSONObject merge(JSONObject a,JSONObject b) {
282        try {Iterator<?> it=b.keys();while(it.hasNext()){String k=(String)it.next();a.put(k,b.get(k));}}
283        catch(JSONException e){throw new IllegalStateException(e);}return a;
284    }
285    public Status status(GUITree tree,GUITreeNode node,String actionType) {
286        Anchor a=anchor(tree,node,actionType);if(a==null)return null;
287        if(concreteExecutions.containsKey(a))return Status.EXECUTED;
288        Entry e=audits.get(a);return e==null?null:e.status;
289    }
290
291    // A read-only reference to the existing runtime authority, not another execution index.
292    private com.android.commands.monkey.ape.model.Model confirmationModel;
293    private Set<GUITreeAction> confirmedInputs=Collections.emptySet();
294    private boolean confirmationTrackingStarted,confirmationHistoryComplete;
295    void confirmationSource(com.android.commands.monkey.ape.model.Model model,Set<GUITreeAction> inputs) {
296        if(!confirmationTrackingStarted) {
297            confirmationHistoryComplete=model!=null && model.getGraph().getTreeHistory().isEmpty();
298            if(model!=null)for(com.android.commands.monkey.ape.model.Model.ActionRecord record:model.getActionHistory())
299                if(record.modelAction.isModelAction() && record.modelAction.requireTarget())confirmationHistoryComplete=false;
300            confirmationTrackingStarted=true;
301        }
302        // Native refinement mutates this Model in place. An unrelated replacement
303        // cannot inherit the original continuous-confirmation completeness claim.
304        else if(confirmationModel!=model || confirmedInputs!=inputs)confirmationHistoryComplete=false;
305        confirmationModel=model;confirmedInputs=inputs;
306    }
307    static final class ConfirmedHistory {
308        final IdentityHashMap<ModelAction,List<GUITreeAction>> groups=new IdentityHashMap<ModelAction,List<GUITreeAction>>();
309        boolean complete;int total,unknown;long enumerationNanos;
310        List<GUITreeAction> members(ModelAction action) {
311            List<GUITreeAction> found=groups.get(action);
312            return found==null?Collections.<GUITreeAction>emptyList():found;
313        }
314    }
315    ConfirmedHistory confirmedHistory(State state) {
316        long profileStart=JevReviewProfile.start();
317        long begin=System.nanoTime();ConfirmedHistory view=new ConfirmedHistory();
318        int before=confirmedInputs.size();
319        view.complete=confirmationHistoryComplete && confirmationModel!=null && confirmationModel.getVersion()==version
320            && JevHistoryAudit.identityContains(confirmationModel.getGraph().getStates(),state);
321        if(confirmationModel==null){view.enumerationNanos=System.nanoTime()-begin;JevReviewProfile.end("history_scan_group_sort",profileStart);return view;}
322        for(GUITreeAction input:confirmedInputs) {
323            JevDeadline.check();view.total++;
324            if(confirmedMappingProblem(input)!=null){view.unknown++;continue;}
325            if(input.getGUITree().getCurrentState()!=state)continue;
326            ModelAction action=input.getModelAction();
327            if(!action.requireTarget())continue;
328            List<GUITreeAction> group=view.groups.get(action);
329            if(group==null){group=new ArrayList<GUITreeAction>();view.groups.put(action,group);}group.add(input);
330        }
331        for(List<GUITreeAction> group:view.groups.values())Collections.sort(group,new Comparator<GUITreeAction>() {
332            public int compare(GUITreeAction a,GUITreeAction b) {
333                int c=Integer.compare(a.getGUITree().getTimestamp(),b.getGUITree().getTimestamp());
334                if(c==0)c=Integer.compare(a.getGUITree().getId(),b.getGUITree().getId());
335                if(c==0)c=ConcreteActionIdentity.of(a.getActionType().name(),a.getGUITreeNode()).toString()
336                    .compareTo(ConcreteActionIdentity.of(b.getActionType().name(),b.getGUITreeNode()).toString());
337                return c;
338            }
339        });
340        view.complete &= view.unknown==0 && before==confirmedInputs.size() && confirmationModel.getVersion()==version;
341        view.enumerationNanos=System.nanoTime()-begin;JevReviewProfile.end("history_scan_group_sort",profileStart);return view;
342    }
343    int confirmationCount(){return confirmedInputs.size();}
344    private boolean confirmedExact(GUITreeAction candidate) {
345        long profileStart=JevReviewProfile.start(),examined=0;
346        try {
347        if(confirmationModel==null)return false;
348        for(GUITreeAction prior:confirmedInputs) {
349            examined++;
350            JevDeadline.check();
351            if(prior.getModelAction()!=candidate.getModelAction())continue;
352            if(confirmedMappingProblem(prior)==null && sameConcrete(candidate,prior))return true;
353        }
354        return false;
355        } finally {JevReviewProfile.end("confirmed_exact",profileStart);JevReviewProfile.add("confirmed_inputs_examined",0,examined);}
356    }
357
358    /** Exact execution in the current State/tree. This never consults the cross-State index. */
359    public boolean hasExactExecution(State state,ModelAction action,GUITree tree,GUITreeNode node) {
360        if(state==null || action==null || tree==null || node==null || tree.getCurrentState()!=state)return false;
361        long profileStart=JevReviewProfile.start();
362        try {GUITreeAction candidate=new GUITreeAction(tree,node,action);
363            return hardMatch(candidate)!=null || confirmedExact(candidate);
364        } finally {JevReviewProfile.end("exact_execution_filter",profileStart);}
365    }
366
367    /** Confirmed concrete execution in any State of the same activity. */
368    public boolean hasRelatedConcreteExecution(String actionType,String sourceActivity,String canonicalFullName) {
369        if(actionType==null || sourceActivity==null || canonicalFullName==null || canonicalFullName.length()==0)return false;
370        ConcreteActionIdentity identity=new ConcreteActionIdentity(actionType,canonicalFullName);
371        List<Execution> values=relatedIndex.get(relatedKey(sourceActivity,identity));
372        if(values!=null && !values.isEmpty())return true;
373        // Input may be confirmed before its transition is observed/indexed.
374        // Consult the existing authority; do not invent another history index.
375        for(GUITreeAction input:confirmedInputs) {
376            if(input.getGUITreeNode()!=null && confirmedMappingProblem(input)==null && input.getActionType().name().equals(actionType)
377                    && sourceActivity.equals(input.getModelAction().getState().getActivity())
378                    && identity.equals(ConcreteActionIdentity.of(actionType,input.getGUITreeNode())))return true;
379        }
380        return false;
381    }
382    private static String relatedKey(String activity,ConcreteActionIdentity identity) {
383        return new JSONArray().put(identity.actionType()).put(activity).put(identity.canonicalFullName()).toString();
384    }
385    public List<Entry> suggestions() {
386        List<Entry> out=new ArrayList<Entry>();
387        for(Entry e:audits.values())if(probeAvailable(e))out.add(e);
388        return out;
389    }
390    private boolean probePending(Entry e) {return e.status==Status.PROBE_SUGGESTED && !e.probeClaimed;}
391    public boolean probeAvailable(Entry e) {return probePending(e) && e.confidence!=null && e.confidence>localProbeConfidence;}
392    /** Reserve this concrete candidate only; siblings keep their independent opportunities. */
393    public boolean claimProbe(Entry entry,long step) {
394        if(!probeAvailable(entry))return false;
395        entry.probeClaimed=true;
396        event("PROBE_CLAIMED",entry,step,merge(entryFields(entry),fields("reason","binding_attempt","selection_version",version)));
397        return true;
398    }
399    public void recordProbeExecution(Entry entry,long step) {
400        entry.probeConfirmed=true;entry.status=Status.EXECUTED;
401        lifecycle(entry,step,"probe_executed",false);
402    }
403    public List<Batch> discover(List<Observation> observations,long step) throws JSONException {
404        long start=System.nanoTime();try{return discoverNow(observations,step);}finally{timing.record("audit_discovery",System.nanoTime()-start);}
405    }
406    private List<Batch> discoverNow(List<Observation> observations,long step) throws JSONException {
407        List<Batch> batches=new ArrayList<Batch>();
408        for(Observation o:observations) {
409            if(!o.valid || o.concreteAction==null || (!o.saturated || o.concreteAction.getModelAction().isUnvisited())
410                    || "BACK".equals(o.type) || "MODEL_BACK".equals(o.type))continue;
411            String source=AFTER_SATURATION;
412            String g=group(version,o.state,o.action);
413            boolean first=seenSaturation.add(g);
414            if(first)events.emit("SATURATION_TRIGGER",
415                step,version,o.state,o.action,fields("action_type",o.type));
416            List<Entry> fresh=new ArrayList<Entry>();
417            List<GUITreeAction> currentActions=new ArrayList<GUITreeAction>();
418            Map<ConcreteActionIdentity,Integer> identityCounts=new HashMap<ConcreteActionIdentity,Integer>();
419            for(int memberIndex=0;memberIndex<o.members.size();memberIndex++) {
420                GUITreeNode node=memberIndex<o.concreteNodes.size()?o.concreteNodes.get(memberIndex):null;
421                if(node!=null) {
422                    ConcreteActionIdentity identity=ConcreteActionIdentity.of(o.members.get(memberIndex).type,node);
423                    Integer count=identityCounts.get(identity);identityCounts.put(identity,count==null?1:count+1);
424                }
425            }
426            for(int memberIndex=0;memberIndex<o.members.size();memberIndex++) {
427                Member m=o.members.get(memberIndex);
428                GUITreeNode concreteNode=memberIndex<o.concreteNodes.size()?o.concreteNodes.get(memberIndex):null;
429                if(concreteNode!=null && !concreteNode.isEnabled())continue;
430                if(concreteNode!=null && identityCounts.get(ConcreteActionIdentity.of(m.type,concreteNode))>1) {
431                    events.emit("CANDIDATE_REJECTED",step,version,o.state,o.action,fields("reason","ambiguous_concrete_identity"));
432                    continue;
433                }
434                Anchor candidateAnchor=anchor(o.sourceTree,concreteNode,m.type);
435                if(candidateAnchor==null || o.concreteAction==null || o.concreteAction.getModelAction()==null) {
436                    events.emit("CANDIDATE_REJECTED",step,version,o.state,o.action,fields("reason","missing_concrete_anchor"));
437                    continue;
438                }
439                GUITreeAction candidateAction=new GUITreeAction(o.sourceTree,concreteNode,o.concreteAction.getModelAction());
440                currentActions.add(candidateAction);
441                Execution hardMatch=hardMatch(candidateAction);
442                if(hardMatch!=null) {
443                    Entry stale=findAudit(candidateAction);
444                    if(stale!=null)retireIfExecuted(stale,step);
445                    dedupCount++;
446                    events.emit("AUDIT_DEDUPLICATED",step,version,o.state,o.action,fields(
447                        "reason","hard_exact_execution","transition_id",hardMatch.transitions.isEmpty()?JSONObject.NULL:hardMatch.transitions.get(0)));
448                    continue;
449                }
450                Entry previous=findAudit(candidateAction);
451                if(previous==null && unresolvedProbe(candidateAction))continue;
452                if(previous!=null)revalidateStructure(previous,o.sourceTree,concreteNode,step);
453                Entry e=previous;
454                if(e==null) {
455                    e=new Entry(version,"e"+(++entrySeq),o.state,o.action,o.sourceTree,candidateAction,m,o.sourceActivity);
456                    audits.put(candidateAnchor,e);addToIndex(auditIndex,candidateAction,e);
457                    ConcreteActionIdentity identity=ConcreteActionIdentity.of(m.type,concreteNode);
458                    List<Entry> identityEntries=identityAudits.get(identity);
459                    if(identityEntries==null){identityEntries=new ArrayList<Entry>();identityAudits.put(identity,identityEntries);}
460                    identityEntries.add(e);entriesById.put(e.id,e);
461                    if(!first)event("NEW_REALIZATION_TRIGGER",e,step,entryFields(e));
462                    event("CANDIDATE_OBSERVED",e,step,entryFields(e));
463                } else if(!e.reviewSource.equals(source) || (e.status!=Status.UNSENT && e.status!=Status.WAITING_HISTORY && !retryableReview(e))) {
464                    dedupCount++;continue;
465                }
466                e.member=m;
467                if(retryableReview(e)){e.status=Status.QUEUED;e.requestId=null;e.unsentLogged=false;}
468                JevReviewLedger.Decision cached=reviewLedger.get(syncReviewContract,
469                    o.sourceActivity,ConcreteActionIdentity.of(m.type,concreteNode));
470                if(cached!=null) {
471                    e.confidence=cached.confidence;
472                    if("PROBE".equals(cached.outcome) && cached.confidence!=null && cached.confidence>localProbeConfidence)e.status=Status.PROBE_SUGGESTED;
473                    else if("REPRESENTED".equals(cached.outcome))e.status=Status.REPRESENTED;
474                    else e.status=Status.RETIRED;
475                    dedupCount++;continue;
476                }
477                JSONObject structure=structuralRepresentation(o.sourceTree,concreteNode,o.type);
478                if(structure!=null) {
479                    e.status=Status.REPRESENTED;e.structural=true;
480                    event("DECISION_REPRESENTED",e,step,merge(merge(entryFields(e),fields("represented_source","structure")),structure));
481                    continue;
482                }
483                e.status=Status.QUEUED;
484                fresh.add(e);
485            }
486            if(fresh.isEmpty())continue;
487            List<Execution> historyExecutions=new ArrayList<Execution>();
488            List<Member> freshMembers=new ArrayList<Member>();for(Entry e:fresh)freshMembers.add(e.member);
489            ModelAction currentModelAction=o.concreteAction==null?null:o.concreteAction.getModelAction();
490            State currentState=o.sourceTree==null?null:o.sourceTree.getCurrentState();
491            JSONObject contexts=new JSONObject();
492            JSONArray history=history(currentActions,o.type,o.sourceActivity,currentState,currentModelAction,freshMembers,historyExecutions,contexts);
493            JSONArray siblings=new JSONArray();int siblingId=0;
494            Map<String,Execution> siblingKeys=new LinkedHashMap<String,Execution>();
495            Map<String,String> contextIds=new LinkedHashMap<String,String>();
496            boolean siblingUnsafe=false;
497            for(int memberIndex=0;memberIndex<o.members.size();memberIndex++) {
498                Member m=o.members.get(memberIndex);String sid="s"+(++siblingId);
499                GUITreeNode node=memberIndex<o.concreteNodes.size()?o.concreteNodes.get(memberIndex):null;
500                Execution x=siblingHistory(new GUITreeAction(o.sourceTree,node,currentModelAction),m);
501                if(x!=null) {
502                    JSONObject view=m.historyView("sibling_id",sid).put("execution_count",x.count)
503                        .put("outcomes",outcomes(x,new HashSet<String>(),contexts,contextIds))
504                        .put("outcomes_truncated",x.outcomes.size()>4);
505                    siblings.put(view);
506                    siblingKeys.put(sid,x);siblingUnsafe|=x.unsafe;
507                }
508            }
509            // Semantic attribution is page-local.  It does not require an
510            // execution history or an observed result to establish ownership.
511            String id=nextRequestId();
512            JSONArray candidates=new JSONArray();boolean unsafe=siblingUnsafe;
513            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;}
514            for(Execution h:historyExecutions)unsafe|=h.unsafe;
515            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);
516            Batch b=new Batch(version,id,o.state,o.action,fresh,state.toString(),historyExecutions,siblingKeys,unsafe);batches.add(b);
517
518        }
519        return batches;
520    }
521    private boolean retryableReview(Entry e) {
522        if(e==null)return false;
523        if(e.status==Status.DELIVERY_FAILED)return true;
524        if(e.status!=Status.RETIRED || e.structural)return false;
525        ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
526        return reviewLedger.get(syncReviewContract,e.sourceActivity,identity)==null;
527    }
528    private JSONObject structuralRepresentation(GUITree tree,GUITreeNode candidate,String type) {
529        if(tree==null || candidate==null)return null;
530        Map<GUITreeNode,Map<String,JSONObject>> nodes=structures.get(tree);
531        if(nodes==null){nodes=new WeakHashMap<GUITreeNode,Map<String,JSONObject>>();structures.put(tree,nodes);}
532        Map<String,JSONObject> results=nodes.get(candidate);
533        if(results==null){results=new HashMap<String,JSONObject>();nodes.put(candidate,results);}
534        if(!results.containsKey(type)) {
535            long start=System.nanoTime();results.put(type,computeStructure(tree,candidate,type,tree.getDisplayBounds()));
536            timing.record("structure",System.nanoTime()-start);timing.count("structure_misses",1);
537        } else timing.count("structure_hits",1);
538        return results.get(type);
539    }
540    private static JSONObject computeStructure(GUITree tree,GUITreeNode candidate,String type,Rect display) {
541        if(!"MODEL_CLICK".equals(type) && !"MODEL_LONG_CLICK".equals(type))return null;
542        if(deepestActionableAtTouch(tree,candidate,type,display)!=null)
543            return fields("reason","touch_masked_by_actionable_descendant");
544        if(tree==null || candidate==null)return null;
545        GUITreeNode root=tree.getRootNode();
546        if(root==null || candidate.getRoot()!=root)return null;
547        Rect clip=root.getBoundsInScreen();
548        // Empty intersection stays empty; the native touch fallback to display is not applicable.
549        if(clip==null || clip.isEmpty())return null;
550        clip=new Rect(clip);
551        if(display!=null && !clip.intersect(display))return null;
552        Rect candidateBounds=clippedBounds(candidate,clip);
553        if(candidateBounds==null)return null;
554
555        Map<GUITreeNode,GUITreeNode> candidateBranches=new IdentityHashMap<GUITreeNode,GUITreeNode>();
556        for(GUITreeNode child=candidate;child.getParent()!=null;child=child.getParent())
557            candidateBranches.put(child.getParent(),child);
558        Deque<GUITreeNode> nodes=new ArrayDeque<GUITreeNode>();nodes.add(root);
559        while(!nodes.isEmpty()) {
560            GUITreeNode foreground=nodes.removeFirst();
561            Iterator<GUITreeNode> children=foreground.getChildren();
562            while(children.hasNext())nodes.addLast(children.next());
563            if(foreground==candidate || candidateBranches.containsKey(foreground)
564                    || !foreground.isEnabled() || !("MODEL_LONG_CLICK".equals(type)?foreground.isLongClickable():foreground.isClickable())
565                    || !Boolean.TRUE.equals(foreground.getJevVisibleToUser()))continue;
566            Rect foregroundBounds=clippedBounds(foreground,clip);
567            if(foregroundBounds==null || !foregroundBounds.contains(candidateBounds))continue;
568            // First shared parent is the LCA. Only its direct children have comparable orders.
569            for(GUITreeNode b=foreground;b.getParent()!=null;b=b.getParent()) {
570                GUITreeNode a=candidateBranches.get(b.getParent());
571                if(a==null)continue;
572                Integer ao=a.getJevDrawingOrder(),bo=b.getJevDrawingOrder();
573                if(a!=b && ao!=null && bo!=null && ao>0 && bo>ao)
574                    return fields("reason","fully_occluded_by_foreground_branch","source_tree_step",tree.getTimestamp(),
575                        // Object identities are the same concrete anchors used by this registry.
576                        "occluded_node",Integer.toHexString(System.identityHashCode(candidate)),
577                        "foreground_node",Integer.toHexString(System.identityHashCode(foreground)),
578                        "occluded_bounds",candidateBounds.flattenToString(),"foreground_bounds",foregroundBounds.flattenToString(),
579                        "occluded_branch_order",ao,"foreground_branch_order",bo);
580                break;
581            }
582        }
583        return null;
584    }
585    private static Rect clippedBounds(GUITreeNode node,Rect clip) {
586        Rect bounds=node.getBoundsInScreen();
587        if(bounds==null || bounds.isEmpty())return null;
588        Rect clipped=new Rect(bounds);
589        return clipped.intersect(clip)?clipped:null;
590    }
591    private static GUITreeNode deepestActionableAtTouch(GUITree tree,GUITreeNode candidate,String type,Rect display) {
592        if(!"MODEL_CLICK".equals(type) && !"MODEL_LONG_CLICK".equals(type))return null;
593        if(tree==null || candidate==null)return null;
594        GUITreeNode root=tree.getRootNode();
595        if(root==null)return null;
596        Rect rootBounds=root.getBoundsInScreen();
597        Rect displayBounds=display==null?rootBounds:display;
598        PointF touch=ApeTouchGeometry.centerClickPoint(candidate.getBoundsInScreen(),rootBounds,displayBounds);
599        return touch==null?null:deepestActionableDescendant(candidate,rootBounds,displayBounds,touch,"MODEL_LONG_CLICK".equals(type));
600    }
601    private static GUITreeNode deepestActionableDescendant(GUITreeNode parent,Rect rootBounds,Rect displayBounds,PointF touch,boolean longClick) {
602        GUITreeNode deepest=null;
603        Iterator<GUITreeNode> children=parent.getChildren();
604        while(children.hasNext()) {
605            GUITreeNode child=children.next();
606            boolean actionable=child.isEnabled() && (longClick?child.isLongClickable():child.isClickable());
607            if(actionable && ApeTouchGeometry.contains(child.getBoundsInScreen(),rootBounds,displayBounds,touch))deepest=child;
608            GUITreeNode nested=deepestActionableDescendant(child,rootBounds,displayBounds,touch,longClick);
609            if(nested!=null && (deepest==null || nested.getDepth()>deepest.getDepth()))deepest=nested;
610        }
611        return deepest;
612    }
613    private void revalidateStructure(Entry e,GUITree tree,GUITreeNode node,long step) {
614        if(e.status==Status.REPRESENTED && e.structural && structuralRepresentation(tree,node,e.member.type)==null) {
615            e.status=Status.UNSENT;e.structural=false;
616            event("REPRESENTATION_INVALIDATED",e,step,merge(entryFields(e),fields("reason","touch_geometry_changed","represented_source","structure")));
617        }
618    }
619    /** Current concrete evidence; only immutable tree geometry is cached. */
620    public Status currentStatus(GUITree tree,GUITreeNode node,ModelAction action,long step) {
621        GUITreeAction concrete=new GUITreeAction(tree,node,action);
622        if(hardMatch(concrete)!=null)return Status.EXECUTED;
623        Entry e=findAudit(concrete);
624        if(e==null)return unresolvedProbe(concrete)?Status.PROBE_SUGGESTED:null;
625        revalidateStructure(e,tree,node,step);
626        return e.status;
627    }
628    /** Ambiguous pending identities supply neither new unknown navigation nor a bindable probe. */
629    private boolean unresolvedProbe(GUITreeAction current) {
630        for(Entry e:indexed(auditIndex,current))if(e.version==version && AFTER_SATURATION.equals(e.reviewSource) && e.status==Status.PROBE_SUGGESTED
631                && e.concreteAction.getModelAction()==current.getModelAction()
632                && e.sourceTree.getCurrentState()==current.getGUITree().getCurrentState()
633                && ConcreteActionIdentity.canonical(e.node).equals(ConcreteActionIdentity.canonical(current.getGUITreeNode())))return true;
634        ConcreteActionIdentity identity=ConcreteActionIdentity.of(current.getActionType().name(),current.getGUITreeNode());
635        List<Entry> matches=identityAudits.get(identity);
636        if(matches!=null)for(Entry e:matches)if(e.version==version && AFTER_SATURATION.equals(e.reviewSource) && e.status==Status.PROBE_SUGGESTED
637                && e.sourceActivity.equals(activityScope(current.getGUITree())))return true;
638        return false;
639    }
640    private Anchor anchor(GUITree tree,GUITreeNode node,String type) {
641        return tree==null || node==null || type==null?null:new Anchor(tree,node,type);
642    }
643    /** One immutable read window only. Never reused across GUI/model mutations. */
644    static final class ConcreteLookup implements AutoCloseable {
645        private static final ThreadLocal<ConcreteLookup> ACTIVE=new ThreadLocal<ConcreteLookup>();
646        private final ConcreteLookup previous=ACTIVE.get();
647        private final IdentityHashMap<GUITreeNode,String> names=new IdentityHashMap<GUITreeNode,String>();
648        private final IdentityHashMap<GUITree,Map<String,GUITreeNode>> trees=new IdentityHashMap<GUITree,Map<String,GUITreeNode>>();
649        private final IdentityHashMap<GUITreeAction,String> mappings=new IdentityHashMap<GUITreeAction,String>();
650        ConcreteLookup(){if(previous==null)ACTIVE.set(this);}
651        static ConcreteLookup current(){return ACTIVE.get();}
652        String canonical(GUITreeNode node){
653            String value=names.get(node);
654            if(value==null){long began=JevReviewProfile.start();value=ConcreteActionIdentity.canonical(node);names.put(node,value);
655                JevReviewProfile.end("concrete_identity",began);}
656            return value;
657        }
658        GUITreeNode find(GUITree tree,ConcreteActionIdentity identity){
659            Map<String,GUITreeNode> nodes=trees.get(tree);
660            if(nodes==null){
661                nodes=new HashMap<String,GUITreeNode>();Deque<GUITreeNode> queue=new ArrayDeque<GUITreeNode>();queue.add(tree.getRootNode());
662                while(!queue.isEmpty()){
663                    JevDeadline.check();GUITreeNode node=queue.removeFirst();String name=canonical(node);
664                    if(nodes.containsKey(name))nodes.put(name,null);else nodes.put(name,node);
665                    Iterator<GUITreeNode> children=node.getChildren();while(children.hasNext())queue.addLast(children.next());
666                }
667                trees.put(tree,nodes);JevReviewProfile.add("identity_tree_index",0,1);
668            }
669            return nodes.get(identity.canonicalFullName());
670        }
671        public void close(){if(previous==null)ACTIVE.remove();}
672    }
673    private static ConcreteActionIdentity lookupIdentity(String type,GUITreeNode node){
674        ConcreteLookup lookup=ConcreteLookup.current();
675        return lookup==null?ConcreteActionIdentity.of(type,node):new ConcreteActionIdentity(type,lookup.canonical(node));
676    }
677    private String confirmedMappingProblem(GUITreeAction input){
678        ConcreteLookup lookup=ConcreteLookup.current();
679        if(lookup==null)return JevHistoryAudit.mappingProblem(input,confirmationModel.getGraph().getStates());
680        if(!lookup.mappings.containsKey(input))lookup.mappings.put(input,JevHistoryAudit.mappingProblem(input,confirmationModel.getGraph().getStates()));
681        return lookup.mappings.get(input);
682    }
683    /** Resolve one identity uniquely in a GUI tree; ambiguity never guesses. */
684    private static GUITreeNode uniqueIdentityNode(GUITree tree,ConcreteActionIdentity identity) {
685        if(tree==null || tree.getRootNode()==null)return null;
686        ConcreteLookup lookup=ConcreteLookup.current();if(lookup!=null)return lookup.find(tree,identity);
687        GUITreeNode found=null;Deque<GUITreeNode> pending=new ArrayDeque<GUITreeNode>();pending.add(tree.getRootNode());
688        while(!pending.isEmpty()) {
689            GUITreeNode node=pending.removeFirst();
690            if(identity.equals(ConcreteActionIdentity.of(identity.actionType(),node))) {
691                if(found!=null)return null;found=node;
692            }
693            Iterator<GUITreeNode> children=node.getChildren();while(children.hasNext())pending.addLast(children.next());
694        }
695        return found;
696    }
697    static GUITreeNode correspondingNode(GUITreeAction source,GUITree tree,ModelAction action) {
698        if(source==null || tree==null || action==null || source.getGUITreeNode()==null)return null;
699        GUITree original=source.getGUITree();GUITreeNode node=source.getGUITreeNode();
700        if(source.getActionType()!=action.getType())return null;
701        ConcreteActionIdentity identity=lookupIdentity(source.getActionType().name(),node);
702        if(original==tree)return uniqueIdentityNode(tree,identity)==node?node:null;
703        if(uniqueIdentityNode(original,identity)!=node)return null;
704        return uniqueIdentityNode(tree,identity);
705    }
706    /** Audit only: count the exact existing identity relation without rebinding or relaxing it. */
707    static JSONObject bindingDiagnostic(final GUITreeAction source,final GUITree tree,final ModelAction action) {
708        try {return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<JSONObject>(){public JSONObject get(){
709            if(source==null||source.getGUITree()==null||source.getGUITreeNode()==null||tree==null||action==null)
710                return fields("reason","missing_source_tree_or_action","source_match_count",-1,"target_match_count",-1);
711            ConcreteActionIdentity identity=ConcreteActionIdentity.of(source.getActionType().name(),source.getGUITreeNode());
712            int original=identityCount(source.getGUITree(),identity),current=identityCount(tree,identity);
713            String reason=source.getActionType()!=action.getType()?"action_type_mismatch":original!=1?"source_identity_not_unique":
714                current==0?"target_node_absent":current>1?"target_node_ambiguous":"unique_identity_match";
715            return fields("reason",reason,"source_match_count",original,"target_match_count",current,"concrete_identity",identity.toString());
716        }});}catch(Exception unavailable){return fields("reason","diagnostic_unavailable","source_match_count",-1,"target_match_count",-1);}
717    }
718    private static int identityCount(GUITree tree,ConcreteActionIdentity identity) {
719        if(tree==null||tree.getRootNode()==null)return 0;
720        int count=0;Deque<GUITreeNode> nodes=new ArrayDeque<GUITreeNode>();nodes.add(tree.getRootNode());
721        while(!nodes.isEmpty()) {GUITreeNode node=nodes.removeFirst();if(identity.equals(ConcreteActionIdentity.of(identity.actionType(),node)))count++;
722            Iterator<GUITreeNode> children=node.getChildren();while(children.hasNext())nodes.addLast(children.next());}
723        return count;
724    }
725    private boolean sameConcrete(GUITreeAction a,GUITreeAction b) {
726        return a!=null && b!=null && a.getActionType()==b.getActionType()
727            && a.getGUITree().getCurrentState()==b.getGUITree().getCurrentState()
728            && correspondingNode(a,b.getGUITree(),b.getModelAction())==b.getGUITreeNode();
729    }
730    private Entry findAudit(GUITreeAction action) {
731        long start=System.nanoTime();try{return findAuditNow(action);}finally{timing.record("history_match",System.nanoTime()-start);}
732    }
733    private Entry findAuditNow(GUITreeAction action) {
734        Entry direct=audits.get(anchor(action.getGUITree(),action.getGUITreeNode(),action.getActionType().name()));
735        if(direct!=null)return direct;
736        for(Entry e:indexed(auditIndex,action))if(e.version==version && sameConcrete(e.concreteAction,action))return e;
737        ConcreteActionIdentity identity=ConcreteActionIdentity.of(action.getActionType().name(),action.getGUITreeNode());
738        List<Entry> matches=identityAudits.get(identity);
739        if(matches!=null)for(Entry e:matches)if(e.sourceActivity.equals(activityScope(action.getGUITree()))
740                && correspondingNode(e.concreteAction,action.getGUITree(),action.getModelAction())!=null)return e;
741        return null;
742    }
743    private Execution hardMatch(GUITreeAction candidate) {
744        long start=System.nanoTime();try{return hardMatchNow(candidate);}finally{timing.record("history_match",System.nanoTime()-start);}
745    }
746    private Execution hardMatchNow(GUITreeAction candidate) {
747        if(candidate==null || candidate.getGUITreeNode()==null)return null;
748        Execution direct=concreteExecutions.get(anchor(candidate.getGUITree(),candidate.getGUITreeNode(),candidate.getActionType().name()));
749        if(direct!=null)return direct;
750        State state=candidate.getGUITree().getCurrentState();
751        List<Execution> matched=new ArrayList<Execution>();
752        for(Execution e:indexed(executionIndex,candidate)) {
753            timing.count("history_candidates",1);
754            if(belongsToState(state,e.anchor.tree) && sameConcrete(candidate,e.concreteAction))matched.add(e);
755        }
756        if(matched.size()==1)return matched.get(0);
757        // HashMap's first concrete receipt is retained when several exact historical facts match.
758        if(matched.size()>1) {
759            refreshExecutionOrder();
760            Execution first=matched.get(0);for(Execution e:matched)if(executionOrder.get(e)<executionOrder.get(first))first=e;return first;
761        }
762        return null;
763    }
764    private void refreshExecutionOrder() {
765        if(executionOrderDirty){int order=0;executionOrder.clear();for(Execution e:concreteExecutions.values())executionOrder.put(e,order++);executionOrderDirty=false;}
766    }
767    boolean retireIfExecuted(Entry e,long step) {
768        Execution match=hardMatch(e.concreteAction);
769        if(match==null)return false;
770        if(e.status!=Status.EXECUTED) {
771            boolean suggestion=e.status==Status.PROBE_SUGGESTED;
772            e.status=Status.EXECUTED;
773            if(suggestion && !e.probeClaimed && e.confidence!=null && e.confidence>localProbeConfidence){
774                admit(e,step);e.naturalConfirmed=true;e.rebuildMatched|=replaying;
775                lifecycle(e,step,replaying?"natural_rebuild_match":"natural_execution",replaying);
776            }
777            event("JEV_RESULT_STALE",e,step,merge(entryFields(e),fields("reason","hard_exact_execution",
778                "transition_id",match.transitions.get(0))));
779        }
780        return true;
781    }
782    /** Combine all confirmed observations of a current sibling without duplicating it in history. */
783    private Execution siblingHistory(GUITreeAction action,Member member) {
784        Execution combined=null;
785        for(Execution e:indexed(executionIndex,action)) {
786            if(!sameConcrete(action,e.concreteAction))continue;
787            if(combined==null)combined=new Execution(member,action.getGUITree().getCurrentState().getActivity(),
788                anchor(action.getGUITree(),action.getGUITreeNode(),member.type),action);
789            mergeExecution(combined,e);
790        }
791        if(combined!=null)Collections.sort(combined.transitions);
792        return combined;
793    }
794    private void mergeExecution(Execution combined,Execution e) {
795        combined.count+=e.count;combined.unsafe|=e.unsafe;combined.transitions.addAll(e.transitions);
796        for(Map.Entry<String,Outcome> pair:e.outcomes.entrySet()) {
797            Outcome old=pair.getValue(),out=combined.outcomes.get(pair.getKey());
798            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);}
799            out.count+=old.count;
800        }
801    }
802    private boolean related(Execution e,String actionType,String sourceActivity,Member candidate) {
803        if(!e.member.type.equals(actionType))return false;
804        if(!e.sourceActivities.contains(sourceActivity==null?"":sourceActivity))return false;
805        return !e.member.fullName.isEmpty() && new ConcreteActionIdentity(e.member.type,e.member.fullName)
806            .equals(new ConcreteActionIdentity(actionType,candidate.fullName));
807    }
808    private boolean relatedToAny(Execution e,String actionType,String sourceActivity,List<Member> candidates) {
809        for(Member candidate:candidates)if(related(e,actionType,sourceActivity,candidate))return true;
810        return false;
811    }
812    private boolean sameStateAction(Execution e,String actionType,State state,ModelAction modelAction) {
813        if(e.anchor==null || !belongsToState(state,e.anchor.tree) || !e.member.type.equals(actionType))return false;
814        return modelAction!=null && e.concreteAction!=null && e.concreteAction.getModelAction()==modelAction;
815    }
816    private boolean belongsToState(State state,GUITree tree) {
817        if(state==null || tree==null)return false;
818        for(GUITree candidate:state.getGUITrees())if(candidate==tree)return true;
819        return false;
820    }
821    private JSONArray history(List<GUITreeAction> currentActions,String actionType,String sourceActivity,State state,ModelAction modelAction,List<Member> candidates,List<Execution> selected,JSONObject contexts) throws JSONException {
822        List<Execution> sameStateAction=new ArrayList<Execution>(), relatedOther=new ArrayList<Execution>();
823        Set<Execution> relevant=Collections.newSetFromMap(new IdentityHashMap<Execution,Boolean>());
824        if(!currentActions.isEmpty())relevant.addAll(indexed(executionIndex,currentActions.get(0)));
825        for(Member member:candidates) {
826            List<Execution> related=relatedIndex.get(relatedKey(sourceActivity==null?"":sourceActivity,
827                new ConcreteActionIdentity(actionType,member.fullName)));
828            if(related!=null)relevant.addAll(related);
829        }
830        for(Execution e:relevant) {
831            boolean current=false;
832            for(GUITreeAction action:currentActions)if(sameConcrete(action,e.concreteAction)){current=true;break;}
833            if(current)continue;
834            if(sameStateAction(e,actionType,state,modelAction))sameStateAction.add(e);
835            else if(belongsToState(state,e.anchor.tree))continue;
836            else if(relatedToAny(e,actionType,sourceActivity,candidates))relatedOther.add(e);
837        }
838        Comparator<Execution> order=new Comparator<Execution>() {public int compare(Execution a,Execution b) {
839            String x=a.transitions.isEmpty()?"":a.transitions.get(0),y=b.transitions.isEmpty()?"":b.transitions.get(0);
840            int compared=x.compareTo(y);if(compared!=0)return compared;
841            refreshExecutionOrder();return Integer.compare(executionOrder.get(a),executionOrder.get(b));
842        }};
843        Collections.sort(sameStateAction,order);Collections.sort(relatedOther,order);
844        sameStateAction.addAll(relatedOther);
845        List<Execution> remaining=new ArrayList<Execution>();
846        for(Execution e:sameStateAction) {
847            Execution combined=null;
848            for(Execution existing:remaining)if(existing.anchor.tree.getCurrentState()==e.anchor.tree.getCurrentState()
849                    && existing.concreteAction.getModelAction()==e.concreteAction.getModelAction()
850                    && existing.member.fullName.equals(e.member.fullName)){combined=existing;break;}
851            if(combined==null) {
852                combined=new Execution(e.member,e.anchor.tree.getCurrentState().getActivity(),e.anchor,e.concreteAction);
853                remaining.add(combined);
854            }
855            mergeExecution(combined,e);
856        }
857        List<Execution> preferred=new ArrayList<Execution>();
858        for(Member candidate:candidates) {
859            boolean matched=false;
860            for(Execution e:preferred)if(related(e,actionType,sourceActivity,candidate)){matched=true;break;}
861            if(matched)continue;
862            for(Execution e:remaining) {
863                if(!belongsToState(state,e.anchor.tree) && related(e,actionType,sourceActivity,candidate)) {
864                    preferred.add(e);break;
865                }
866            }
867            if(preferred.size()==8)break;
868        }
869        Set<String> covered=new HashSet<String>();JSONArray out=new JSONArray();
870        Map<String,String> contextIds=new LinkedHashMap<String,String>();
871        while(!remaining.isEmpty() && selected.size()<8) {
872            // v4 sends only concrete control evidence. Keep the deterministic
873            // related-first order and transition order; post-execution outcome
874            // novelty no longer selects what the model is shown.
875            Execution best=preferred.isEmpty()?remaining.get(0):preferred.remove(0);
876            remaining.remove(best);selected.add(best);
877            JSONObject context=JevEvidence.sourceContext(best.anchor.tree);
878            // All contexts use the same field construction order; compare the complete serialized value.
879            String key=context.toString(),contextId=contextIds.get(key);
880            if(contextId==null) {
881                contextId="p"+(contexts.length()+1);contextIds.put(key,contextId);contexts.put(contextId,context);
882            }
883            out.put(best.member.historyView("member_id","h"+selected.size()).put("source_context_id",contextId).put("execution_count",best.count)
884                .put("outcomes",outcomes(best,covered,contexts,contextIds)).put("outcomes_truncated",best.outcomes.size()>4));
885        }return out;
886    }
887    private String contextId(String context,JSONObject contexts,Map<String,String> ids)throws JSONException {
888        String id=ids.get(context);if(id!=null)return id;
889        id="p"+(contexts.length()+1);ids.put(context,id);contexts.put(id,new JSONObject(context));return id;
890    }
891    private JSONArray outcomes(Execution e,Set<String> covered,JSONObject contexts,Map<String,String> contextIds)throws JSONException {
892        JSONArray out=new JSONArray();List<String> selected=new ArrayList<String>();
893        for(String outcome:e.outcomes.keySet())if(!covered.contains(outcome) && selected.size()<4)selected.add(outcome);
894        for(String outcome:e.outcomes.keySet())if(!selected.contains(outcome) && selected.size()<4)selected.add(outcome);
895        for(String outcome:selected){Outcome value=e.outcomes.get(outcome);out.put(value.view(contextId(value.sourceContext,contexts,contextIds)));covered.add(outcome);}return out;
896    }
897    public Batch page(List<Batch> groups,int maxBytes,String model) throws JSONException {
898        setSemanticReviewModel(model);
899        List<Entry> entries=new ArrayList<Entry>();
900        List<Execution> history=new ArrayList<Execution>();boolean unsafe=false;
901        for(Batch b:groups){entries.addAll(b.entries);history.addAll(b.history);unsafe|=b.unsafe;}
902        String id=nextRequestId();
903        State state=entries.get(0).concreteAction.getModelAction().getState();
904        GUITree tree=entries.get(0).sourceTree;
905        JSONObject page=new JSONObject().put("state_id",state.getGraphId()).put("activity",state.getActivity());
906        JSONArray modelActions=new JSONArray(),controls=new JSONArray();
907        Map<String,String> candidateIds=new LinkedHashMap<String,String>();int candidate=0;
908        for(Entry e:entries)candidateIds.put(e.action+"|"+ConcreteActionIdentity.canonical(e.node),"c"+(++candidate));
909        Map<String,Entry> entriesByCandidate=new LinkedHashMap<String,Entry>();
910        for(Entry e:entries)entriesByCandidate.put(candidateIds.get(e.action+"|"+ConcreteActionIdentity.canonical(e.node)),e);
911        for(ModelAction action:state.getActions()) {
912            if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled()||action.getResolvedGUITree()!=tree)continue;
913            GUITreeNode[] nodes=action.getResolvedNodes();if(nodes==null||nodes.length==0)continue;
914            JSONArray executed=new JSONArray(),unexecuted=new JSONArray();
915            for(GUITreeNode node:nodes) {
916                JSONObject control=JevSemanticReview.node(node,action.getType().name());
917                String key=action.getGraphId()+"|"+ConcreteActionIdentity.canonical(node);
918                if(candidateIds.containsKey(key)) {
919                    control.put("candidate_id",candidateIds.get(key));unexecuted.put(control);
920                } else if(hardMatch(new GUITreeAction(tree,node,action))!=null)executed.put(control);
921                else unexecuted.put(control);
922                control.put("model_action_id",action.getGraphId());controls.put(control);
923            }
924            modelActions.put(new JSONObject().put("model_action_id",action.getGraphId()).put("action_type",action.getType().name())
925                .put("executed_concrete_actions",executed).put("unexecuted_concrete_actions",unexecuted));
926        }
927        List<android.graphics.Rect> pageBounds=new ArrayList<android.graphics.Rect>();
928        for(int i=0;i<controls.length();i++) {
929            JSONArray b=controls.getJSONObject(i).getJSONArray("bounds");
930            pageBounds.add(new android.graphics.Rect(b.getInt(0),b.getInt(1),b.getInt(2),b.getInt(3)));
931        }
932        // Keep the same pairwise spatial evidence as asynchronous review.
933        for(int i=0;i<controls.length();i++) {
934            JSONObject control=controls.getJSONObject(i);JSONArray relations=new JSONArray();
935            android.graphics.Rect from=pageBounds.get(i);
936            for(int j=0;j<pageBounds.size();j++)if(i!=j) {
937                android.graphics.Rect to=pageBounds.get(j);String relation;
938                if(from.right<=to.left)relation="left_of";else if(from.left>=to.right)relation="right_of";
939                else if(from.bottom<=to.top)relation="above";else if(from.top>=to.bottom)relation="below";else relation="overlaps";
940                relations.put(new JSONObject().put("relation",relation).put("control_index",j));
941            }
942            control.put("spatial_relations",relations);
943        }
944        JSONArray selectedIds=new JSONArray();for(String value:candidateIds.values())selectedIds.put(value);
945        page.put("model_actions",modelActions).put("page_controls",controls).put("candidate_ids",selectedIds);
946        page.put("input_signature",JevEvidence.digest(page.toString()));
947        JevProtocol.Request request=JevProtocol.semantic(id,model,page,maxBytes);
948        Batch pageBatch=new Batch(version,id,groups.get(0).state,null,entries,request.stateJson,history,Collections.<String,Execution>emptyMap(),unsafe);
949        pageBatch.request=request;pageBatch.semanticEntries=Collections.unmodifiableMap(entriesByCandidate);pageBatch.pageGroups=Collections.unmodifiableList(new ArrayList<Batch>(groups));
950        for(Entry e:entries) {
951            e.requestId=id;
952            ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
953            reviewLedger.bind(syncReviewContract,e.sourceActivity,identity,JevEvidence.guiIdentity(e.sourceTree));
954        }
955        return pageBatch;
956    }
957    /** Bind a completed decision to the current GUI only after unique identity matching. */
958    public void bindCurrent(Batch b,GUITree currentTree) {
959        if(b==null || currentTree==null)return;
960        for(Entry e:b.entries) {
961            ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
962            if(e.sourceActivity.equals(activityScope(currentTree)) && uniqueIdentityNode(currentTree,identity)!=null)
963                reviewLedger.bind(syncReviewContract,e.sourceActivity,identity,JevEvidence.guiIdentity(currentTree));
964        }
965    }
966    public JSONObject receipt(Batch b) throws JSONException {
967        JSONArray candidates=new JSONArray(),history=new JSONArray(),groups=new JSONArray();
968        for(Batch group:b.pageGroups)groups.put(receipt(group));
969        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));}
970        for(int i=0;i<b.history.size();i++){Execution e=b.history.get(i);
971            JSONArray transitions=new JSONArray();for(String t:e.transitions)transitions.put(t);
972            history.put(fields("member_id","h"+(i+1),"transition_ids",transitions));}
973        JSONArray siblings=new JSONArray();
974        for(Map.Entry<String,Execution> pair:b.executedSiblings.entrySet()) {
975            Execution e=pair.getValue();JSONArray transitions=new JSONArray();for(String t:e.transitions)transitions.put(t);
976            siblings.put(fields("sibling_id",pair.getKey(),"transition_ids",transitions));
977        }
978        JSONObject receipt=fields("request_id",b.id,"abstraction_version",b.version,"state_id",b.state,"model_action_id",b.action,
979            "sampling_policy",SAMPLING,"groups",groups,"candidates",candidates,"current_siblings",siblings,"executed_history_other_pages",history);
980        if(b.request!=null)receipt.put("local_target_states",new JSONObject(b.request.localTargetStatesJson));
981        return receipt;
982    }
983    public JSONObject requestFields(Batch b) {
984        return fields("request_id",b.id,"review_source",b.reviewSource,"candidate_count",b.entries.size(),"history_count",b.history.size()+b.executedSiblings.size(),
985            "request_bytes",b.request==null?JSONObject.NULL:b.request.requestBytes,
986            "evidence_digest",b.request==null?JevEvidence.digest(b.stateJson):b.request.evidenceDigest,"sampling_policy",SAMPLING);
987    }
988    /** Apply a complete request-fingerprint hit without another provider call. */
989    public boolean reuseFingerprint(Batch b,long step) {
990        if(b==null || b.request==null)return false;
991        String scope=b.entries.isEmpty()?"":b.entries.get(0).sourceActivity;
992        Map<ConcreteActionIdentity,JevReviewLedger.Decision> hits=reviewLedger.byFingerprint(syncReviewContract,scope,b.request.inputFingerprint);
993        if(hits.isEmpty())return false;
994        for(Entry e:b.entries) {
995            ConcreteActionIdentity identity=ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node);
996            if(!hits.containsKey(identity))return false;
997        }
998        for(Entry e:b.entries) {
999            JevReviewLedger.Decision decision=hits.get(ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node));
1000            e.requestId=b.id;e.confidence=decision.confidence;e.decisionStep=step;e.decisionNanos=System.nanoTime();
1001            if("PROBE".equals(decision.outcome))e.status=Status.PROBE_SUGGESTED;
1002            else if("EXECUTED".equals(decision.outcome))e.status=Status.EXECUTED;
1003            else e.status=Status.REPRESENTED;
1004            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)));
1005        }
1006        return true;
1007    }
1008    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));}
1009
1010    /** Emit an asynchronous semantic decision with the same run-local identity
1011     * fields used by the synchronous audit path.  This is evidence of a real
1012     * parsed provider result, not a derived counter. */
1013    public void semanticEvent(String name,long step,String state,String action,JSONObject fields) {
1014        events.emit(name,step,version,state,action,fields);
1015    }
1016    public void apply(Batch b,String raw,long step) throws JSONException {
1017        long start=System.nanoTime();try{applyNow(b,raw,step);}finally{timing.record("audit_apply",System.nanoTime()-start);}
1018    }
1019    private void applyNow(Batch b,String raw,long step) throws JSONException {
1020        if(b.completed)return;
1021        List<String> ids=new ArrayList<String>();for(int i=0;i<b.entries.size();i++)ids.add("c"+(i+1));
1022        JevProtocol.Result result=b.request!=null&&b.request.semantic?JevProtocol.fromProvider(raw,b.request):JevProtocol.validate(raw,ids); // Validate before first mutation.
1023        b.completed=true;
1024        for(int i=0;i<b.entries.size();i++) {
1025            Entry e=b.request!=null&&b.request.semantic?b.semanticEntries.get("c"+(i+1)):b.entries.get(i);
1026            if(e==null)throw new JSONException("missing semantic candidate binding");
1027            JevProtocol.Decision d=result.decisions.get("c"+(i+1));
1028            boolean current=b.id.equals(e.requestId) && e.status==Status.QUEUED;
1029            if(retireIfExecuted(e,step) || !current) {
1030                event("JEV_RESULT_STALE",e,step,merge(entryFields(e),fields("request_id",b.id,"review_source",b.reviewSource,"reason","execution_or_superseded_review")));continue;
1031            }
1032            e.decisionStep=step;e.decisionNanos=System.nanoTime();e.confidence=d.confidence;
1033            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));
1034            String cachedOutcome;
1035            if(d.confidence==null || d.confidence<=localProbeConfidence) {
1036                e.status=Status.RETIRED;
1037                cachedOutcome="REVIEW_FAILED";
1038                event("AUDIT_RETIRED",e,step,merge(f,fields("reason",d.confidence==null?"confidence_missing":"low_confidence","retired",true)));
1039            } else if(b.request!=null&&b.request.semantic?JevSemanticProtocol.PROMOTE.equals(d.representativeModelActionId):"PROBE".equals(d.decision)) {
1040                e.status=Status.PROBE_SUGGESTED;event("DECISION_PROBE",e,step,f);
1041                event("PROBE_SUGGESTED",e,step,entryFields(e));
1042                cachedOutcome="PROBE";
1043            }
1044            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";}
1045            // Failed/low-confidence reviews remain retryable; only a valid
1046            // PROBE or REPRESENTED conclusion enters the reusable ledger.
1047            if(!"REVIEW_FAILED".equals(cachedOutcome))
1048                reviewLedger.put(syncReviewContract,e.sourceActivity,ConcreteActionIdentity.of(e.concreteAction.getActionType().name(),e.node),
1049                    cachedOutcome,d.confidence,d.representativeModelActionId,b.request==null?null:b.request.inputFingerprint);
1050        }
1051    }
1052    public void selected(Entry e,long step) {
1053        admit(e,step);
1054        event("PROBE_SELECTED",e,step,merge(entryFields(e),fields("decision_step",e.decisionStep,"decision_monotonic_ns",e.decisionNanos)));}
1055    private void admit(Entry e,long step) {
1056        if(e.additionalAdmitted)return;
1057        e.additionalAdmitted=true;admittedProbes.put(e.id,e);lifecycle(e,step,"admitted",false);
1058    }
1059    public void failed(Entry e,long step,String reason) {
1060        if(e.status!=Status.PROBE_SUGGESTED)return;
1061        e.probeClaimed=false;
1062        event("PROBE_EXECUTION_FAILED",e,step,merge(entryFields(e),fields("reason",reason,"retired",false)));
1063    }
1064    public void unsent(Batch b,long step) {
1065        unsentDeferrals++;
1066        for(Entry e:b.entries) if(b.id.equals(e.requestId) && e.status==Status.QUEUED) {
1067            e.status=Status.UNSENT;
1068            // Emit only the first transition to unsent, not on every busy safe point.
1069            if(!e.unsentLogged){event("AUDIT_UNSENT",e,step,entryFields(e));e.unsentLogged=true;}
1070            e.requestId=null;
1071        }
1072    }
1073    public void admitted(Batch b,long step) {
1074        for(Entry e:b.entries)if(b.id.equals(e.requestId) && e.status==Status.QUEUED) {
1075            event("AUDIT_ACCEPTED",e,step,entryFields(e));
1076        }
1077    }
1078    public void deliveryFailed(Batch b,long step,String reason) {
1079        if(b.completed)return;
1080        b.completed=true;
1081        for(Entry e:b.entries) if((e.requestId==null || b.id.equals(e.requestId)) && (e.status==Status.QUEUED || e.status==Status.UNSENT)) {
1082            e.status=Status.DELIVERY_FAILED;event("AUDIT_DELIVERY_FAILED",e,step,merge(entryFields(e),fields("reason",reason)));
1083        }
1084    }
1085    private void lifecycle(Entry e,long step,String terminal,boolean rebuild) {
1086        events.emit("PROBE_LIFECYCLE",step,e.version,e.state,e.action,merge(entryFields(e),fields("terminal",terminal,
1087            "admitted",e.additionalAdmitted,"natural_execution",e.naturalConfirmed,"rebuild_match",rebuild)));
1088    }
1089    void finishAccounting(long step) {
1090        int batch=0;
1091        for(Entry e:admittedProbes.values()) {
1092            String terminal=e.probeConfirmed?"probe_executed":e.naturalConfirmed?"natural_execution":
1093                e.terminalReason!=null?e.terminalReason:probePending(e)?"unclassified":
1094                e.status==Status.RETIRED || e.status==Status.DELIVERY_FAILED?"failed_or_interrupted":"unclassified";
1095            events.emit("PROBE_ACCOUNTING_FINAL",step,e.version,e.state,e.action,merge(entryFields(e),fields("terminal",terminal,
1096                "admitted",true,"rebuild_match",e.rebuildMatched,"reason",e.terminalReason==null?e.status.name():e.terminalReason)));
1097            if(++batch%32==0)events.flush();
1098        }
1099    }
1100    public void counters(long step) {
1101        events.emit("JEV_COUNTERS",step,version,null,null,fields("dedup_count",dedupCount,"unsent_deferrals",unsentDeferrals));
1102    }
1103    public Batch slice(Batch source,int from,int to) throws JSONException {
1104        String id=nextRequestId();List<Entry> entries=new ArrayList<Entry>(source.entries.subList(from,to));
1105        JSONArray candidates=new JSONArray();
1106        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)));}
1107        JSONObject state=new JSONObject(source.stateJson);state.put("candidates",candidates);
1108        return new Batch(source.version,id,source.state,source.action,entries,state.toString(),source.history,source.executedSiblings,source.unsafe);
1109    }
1110    public void executed(String s,String a,Member member,GUITreeAction concreteAction,String sourceActivity,String targetState,String targetActivity,JSONArray labels,String transition,
1111            long step,boolean newState,boolean newAction,boolean refinement,boolean selectedProbe,boolean replay,boolean unsafeResult,Boolean guiUnchanged) {
1112        executed(s,a,member,concreteAction,null,sourceActivity,targetState,targetActivity,labels,transition,step,newState,newAction,refinement,selectedProbe,replay,unsafeResult,guiUnchanged);
1113    }
1114    public void executed(String s,String a,Member member,GUITreeAction concreteAction,GUITree targetTree,String sourceActivity,String targetState,String targetActivity,JSONArray labels,String transition,
1115            long step,boolean newState,boolean newAction,boolean refinement,boolean selectedProbe,boolean replay,boolean unsafeResult,Boolean guiUnchanged) {
1116        Anchor anchor=concreteAction==null?null:anchor(concreteAction.getGUITree(),concreteAction.getGUITreeNode(),member.type);
1117        if(anchor==null)throw new IllegalArgumentException("confirmed execution requires concrete APE anchor");
1118        boolean newActivity=false;
1119        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);
1120        if(newActivity) {
1121            String key=relatedKey(sourceActivity==null?"":sourceActivity,
1122                concreteAction.getGUITreeNode()==null?new ConcreteActionIdentity(member.type,member.fullName):ConcreteActionIdentity.of(member.type,concreteAction.getGUITreeNode()));
1123            List<Execution> values=relatedIndex.get(key);if(values==null){values=new ArrayList<Execution>();relatedIndex.put(key,values);}values.add(x);
1124        }
1125        changed(concreteAction.getModelAction());
1126        x.count++;x.unsafe|=unsafeResult;
1127        reviewLedger.markExecutedMatching(null,concreteAction.getGUITree().getCurrentState(),
1128            concreteAction.getGUITreeNode()==null?new ConcreteActionIdentity(member.type,member.fullName):ConcreteActionIdentity.of(member.type,concreteAction.getGUITreeNode()));
1129        boolean selfLoop=s.equals(targetState),activityUnchanged=Objects.equals(sourceActivity,targetActivity);
1130        x.transitions.add(transition);
1131        String sourceContext=JevEvidence.sourceContext(anchor.tree).toString();
1132        String localChange=localChange(concreteAction.getGUITreeNode(),targetTree);
1133        String ok=new JSONArray().put(sourceContext).put(targetActivity).put(targetState).put(labels).put(selfLoop).put(activityUnchanged).put(guiUnchanged).put(localChange).toString();
1134        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++;
1135        Entry e=audits.get(anchor);
1136        replaying=replay;
1137        try{for(Entry audited:indexed(auditIndex,concreteAction))if(sameConcrete(audited.concreteAction,concreteAction))retireIfExecuted(audited,step);}
1138        finally{replaying=false;}
1139        if(!replay) {
1140            JSONObject f=fields("transition_id",transition,"new_state",newState,"new_model_action",newAction,"refinement",refinement,
1141                "self_loop",selfLoop,"activity_unchanged",activityUnchanged,"gui_unchanged",guiUnchanged,
1142                "target_state",targetState,"input_confirmed",true);
1143            if(e!=null)merge(f,entryFields(e));
1144            events.emit("NATIVE_TRANSITION",step,version,s,a,f);
1145            if(selectedProbe && e!=null)event("PROBE_EXECUTED",e,step,merge(f,fields("decision_step",e.decisionStep,"decision_monotonic_ns",e.decisionNanos)));
1146        }
1147    }
1148    private static String localChange(GUITreeNode source,GUITree target) {
1149        if(source==null || target==null)return null;
1150        List<GUITreeNode> matches=new ArrayList<GUITreeNode>();collectSameName(target.getRootNode(),JevIdentity.fullName(source).toXPath(),matches);
1151        if(matches.size()!=1)return null;
1152        GUITreeNode after=matches.get(0);JSONObject change=new JSONObject();
1153        try {
1154            if(source.isCheckable() && after.isCheckable() && source.isChecked()!=after.isChecked())
1155                change.put("checked",new JSONObject().put("before",source.isChecked()).put("after",after.isChecked()));
1156            Boolean before=source.getJevSelected(),afterSelected=after.getJevSelected();
1157            if(before!=null && afterSelected!=null && !before.equals(afterSelected))
1158                change.put("selected",new JSONObject().put("before",before).put("after",afterSelected));
1159            return change.length()==0?null:change.toString();
1160        } catch(JSONException e){return null;}
1161    }
1162    private static void collectSameName(GUITreeNode node,String name,List<GUITreeNode> out) {
1163        if(node==null || out.size()>1)return;
1164        if(JevIdentity.fullName(node).toXPath().equals(name))out.add(node);
1165        Iterator<GUITreeNode> it=node.getChildren();while(it.hasNext())collectSameName(it.next(),name,out);
1166    }
1167}