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}