JevSemanticReview.java
原始文件
1package com.android.commands.monkey.ape.jev;
2
3import java.io.IOException;
4import java.util.ArrayList;
5import java.util.Collections;
6import java.util.HashMap;
7import java.util.HashSet;
8import java.util.Iterator;
9import java.util.IdentityHashMap;
10import java.util.LinkedHashMap;
11import java.util.LinkedHashSet;
12import java.util.List;
13import java.util.Map;
14import java.util.Set;
15import java.util.concurrent.*;
16import java.util.concurrent.locks.ReentrantLock;
17import java.util.concurrent.atomic.AtomicLong;
18import org.json.JSONArray;
19import org.json.JSONException;
20import org.json.JSONObject;
21import android.graphics.Rect;
22import com.android.commands.monkey.ape.model.ModelAction;
23import com.android.commands.monkey.ape.model.State;
24import com.android.commands.monkey.ape.tree.GUITree;
25import com.android.commands.monkey.ape.tree.GUITreeNode;
26import com.android.commands.monkey.ape.utils.Config;
27
28/** Candidate versus same-group confirmed history; Native scheduling remains outside this class. */
29public final class JevSemanticReview implements AutoCloseable {
30 public static final int MIN_CONCRETE_ACTIONS=3;
31 private static final int MAX_CANDIDATES_PER_ROUND=3;
32 private static final int MAX_RETRIES=1;
33 public interface ReceiptSink { boolean save(String name,JSONObject receipt); }
34 public static final class Candidate {
35 public final ModelAction action;
36 public final GUITreeNode node;
37 public final String key;
38 public final String candidateId;
39 String evidenceScope;
40 String reviewContract;
41 ConcreteActionIdentity concreteIdentity;
42 JevReviewLedger.Staged staged;
43 GUITree auditSource;
44 final Map<String,com.android.commands.monkey.ape.tree.GUITreeAction> auditHistory=new LinkedHashMap<String,com.android.commands.monkey.ape.tree.GUITreeAction>();
45 Candidate(ModelAction a,GUITreeNode n,String k,String id){action=a;node=n;key=k;candidateId=id;}
46 }
47 public static final class Selection {
48 public final Candidate candidate;
49 public final String entryId,requestId,candidateId;
50 public final String concreteIdentity,decisionScope,inputSignature;
51 public final double confidence;
52 public final String representativeModelActionId;
53 Selection(Candidate c,String entry,String request,double score,String representative) {
54 candidate=c;entryId=entry;requestId=request;candidateId=c.staged==null?c.candidateId:c.staged.decision.candidateId;confidence=score;representativeModelActionId=representative;
55 concreteIdentity=JevEvidence.digest(identity(c).toString());decisionScope=JevEvidence.digest(scope(c));
56 inputSignature=c.evidenceScope==null?null:c.evidenceScope.substring(c.evidenceScope.lastIndexOf('|')+1);
57 }
58 }
59 private static final class Cache {
60 final Map<String,Candidate> candidates;
61 final Map<String,Candidate> byId;
62 final JevHistoryPairwiseProtocol.Request request;
63 final JevTransport.Call call;
64 final int attempt;
65 final String sourceGui;
66 final State sourceState;
67 final Map<String,JevHistoryPairwiseProtocol.Decision> decisions=new HashMap<String,JevHistoryPairwiseProtocol.Decision>();
68 boolean valid;
69 boolean pairCacheHit;
70 long sourceVersion,historyRevision;int confirmationCount;
71 Cache(Map<String,Candidate> c,JevHistoryPairwiseProtocol.Request r,JevTransport.Call q,int a,String gui){candidates=c;request=r;call=q;attempt=a;sourceGui=gui;sourceState=c.isEmpty()?null:c.values().iterator().next().action.getState();
72 byId=new HashMap<String,Candidate>();for(Candidate candidate:c.values())byId.put(candidate.candidateId,candidate);}
73 }
74 private final JevTransport transport;
75 private final JevRegistry registry;
76 private final JevSemanticNavigationMask navigationMask;
77 private final String model;
78 private final String endpoint;
79 private final int timeoutMs,maxBytes,requestLifetimeMs,retainedCapacity;
80 private final AtomicLong sequence=new AtomicLong();
81 private final Map<String,Cache> pages=new HashMap<String,Cache>();
82 // Completed raw responses stay on their Call. Only the APE thread may validate/apply them.
83 private final Map<String,Cache> retained=new LinkedHashMap<String,Cache>();
84 private long modelVersion=-1;
85 private final Set<String> inFlight=new HashSet<String>();
86 private final Set<String> served=new HashSet<String>();
87 /* Run-local cursor for candidates that remain eligible after a bounded
88 review. The ledger remains the only durable decision store. */
89 private final Map<String,Integer> incrementalOffsets=new HashMap<String,Integer>();
90 private final String contract;
91 private final Map<String,Integer> fingerprintAttempts=new HashMap<String,Integer>();
92 private final ReceiptSink receiptSink;
93 private JevEvidenceAudit evidenceAudit;
94 private com.android.commands.monkey.MonkeySourceApe auditApe;
95 void setEvidenceAudit(JevEvidenceAudit audit,com.android.commands.monkey.MonkeySourceApe ape){evidenceAudit=audit;auditApe=ape;}
96 private final boolean synchronous;
97 private volatile boolean healthy=true;
98 private volatile boolean selectionBlocked,localFailure;
99 private JevRuntime.Validator nativeValidator;
100 private final ReentrantLock reviewLock=new ReentrantLock();
101 // Only copied receipts / provider text enter this worker. Never GUI or Registry mutations.
102 private final ThreadPoolExecutor receiptWorker=new ThreadPoolExecutor(1,1,0,TimeUnit.MILLISECONDS,
103 new ArrayBlockingQueue<Runnable>(4),new ThreadFactory(){public Thread newThread(Runnable r){
104 Thread t=new Thread(r,"ape-semantic-local");t.setDaemon(true);return t;}});
105 private volatile long currentStep;
106 private String reviewId;
107 private long reviewEntryNanos;
108 private boolean closed;
109 private long localEvidenceNanos,providerWaitNanos;
110 private Page preparedPage;private State preparedState;private GUITree preparedTree;
111
112 public JevSemanticReview(String endpoint,String token,int queueCapacity,int timeoutMs,int maxBytes,String model,JevRegistry history)throws IOException {
113 this(endpoint,token,queueCapacity,timeoutMs,maxBytes,model,history,null,true);
114 }
115 public JevSemanticReview(String endpoint,String token,int queueCapacity,int timeoutMs,int maxBytes,String model,JevRegistry history,ReceiptSink sink)throws IOException {
116 this(endpoint,token,queueCapacity,timeoutMs,maxBytes,model,history,sink,true);
117 }
118 public JevSemanticReview(String endpoint,String token,int queueCapacity,int timeoutMs,int maxBytes,String model,JevRegistry history,ReceiptSink sink,boolean waitForResponse)throws IOException {
119 transport=new JevTransport(endpoint,token,queueCapacity,"probe");
120 registry=history;navigationMask=new JevSemanticNavigationMask(history);this.endpoint=endpoint;this.model=model;this.contract=JevHistoryPairwiseProtocol.VERSION+":"+JevHistoryPairwiseProtocol.POLICY+":"+model;this.timeoutMs=timeoutMs;this.maxBytes=maxBytes;receiptSink=sink;synchronous=waitForResponse;
121 requestLifetimeMs=Math.max(timeoutMs,Config.getInteger("ape.jev.timeoutMs",60000));
122 retainedCapacity=queueCapacity+1;
123 }
124 public boolean isSynchronous(){return synchronous;}
125 java.util.List<JevReviewLedger.PendingProbe> pendingNavigationProbes(State state) {
126 return registry.reviewLedger().pendingProbes(contract,state);
127 }
128 public boolean isHealthy(){return healthy;}
129 public boolean hasLocalFailure(){return localFailure;}
130 public void setStep(long step){currentStep=step;}
131 void setReviewId(String id,long entryNanos){reviewId=id;reviewEntryNanos=entryNanos;localEvidenceNanos=0;providerWaitNanos=0;}
132 long localEvidenceNanos(){return localEvidenceNanos;}
133 long providerWaitNanos(){return providerWaitNanos;}
134 public void setModelVersion(long version) {reviewLock.lock();try {
135 long previous=modelVersion;
136 modelVersion=version;
137 if(previous!=version && preparedPage!=null)contextInvalidated(preparedPage,"model_version_changed");
138 // Existing terminal ledger semantics stay unchanged. Unapplied retained
139 // responses require this exact version again before they can be consumed.
140 } finally {reviewLock.unlock();}}
141 public void setSnapshotIdentity(Map<String,String> identity) {reviewLock.lock();try { transport.setSnapshotIdentity(identity); } finally {reviewLock.unlock();}}
142 public void markExecuted(State state,ModelAction action,GUITreeNode node) {reviewLock.lock();try {
143 if(state==null||action==null||node==null)return;
144 String key=key(action,node);
145 ConcreteActionIdentity executedIdentity=ConcreteActionIdentity.of(action.getType().name(),node);
146 registry.reviewLedger().markExecutedMatching(contract,state,executedIdentity);
147 // Keep immutable request bindings intact; late replies recheck confirmed execution.
148 for(Cache cache:pages.values())cache.decisions.remove(key);
149 inFlight.remove(key);
150 served.remove(key);
151 } finally {reviewLock.unlock();}}
152 /** Review the current page before APE is allowed to select an action. */
153 public void prepare(State state,GUITree tree) { prepare(state,tree,currentStep); }
154 public void prepare(State state,GUITree tree,long step) {prepare(state,tree,step,null);}
155 void prepare(State state,GUITree tree,long step,JevRuntime.Validator validator) {
156 long prepareStart=JevReviewProfile.start();
157 // This flag describes this invocation's construction error, not a prior round.
158 localFailure=false;localEvidenceNanos=0;providerWaitNanos=0;
159 final long absolute=System.nanoTime()+TimeUnit.MILLISECONDS.toNanos(timeoutMs);
160 try (JevDeadline budget=JevDeadline.enter(absolute)) {
161 long lockStart=JevReviewProfile.start();
162 try {
163 if(!reviewLock.tryLock(Math.max(0,absolute-System.nanoTime()),TimeUnit.NANOSECONDS)) {
164 healthy=false;selectionBlocked=true;return;
165 }
166 } catch(InterruptedException e) {Thread.currentThread().interrupt();healthy=false;selectionBlocked=true;return;}
167 finally {JevReviewProfile.end("review_lock_wait",lockStart);}
168 try {
169 currentStep=step;nativeValidator=validator;
170 if(reviewId!=null)registry.semanticEvent("SEMANTIC_REVIEW_STARTED",step,state==null?null:state.getGraphId(),null,
171 JevRegistry.fields("review_id",reviewId,"review_entry_ns",reviewEntryNanos));
172 try(JevRegistry.ConcreteLookup lookup=new JevRegistry.ConcreteLookup()) {prepareLocked(state,tree,absolute);}
173 }
174 catch(JevDeadline.Expired e) {
175 for(Cache cache:pages.values())retain(cache);
176 pages.clear();inFlight.clear();
177 healthy=false;selectionBlocked=true;
178 try {failOpen(state,"DEADLINE");}catch(JevDeadline.Expired ignored) { /* audit lock busy: do not wait again */ }
179 } finally {nativeValidator=null;reviewLock.unlock();}
180 } finally {JevReviewProfile.end("prepare_total",prepareStart);}
181 }
182 private void prepareLocked(State state,GUITree tree,long deadline) {
183 selectionBlocked=false;localFailure=false;
184 Page previous=preparedPage;preparedPage=null;
185 if(previous!=null)contextInvalidated(previous,"prepare_reentered");
186 if(state==null||tree==null||tree.getCurrentState()!=state){JevReviewProfile.add("return_invalid_context",0,1);return;}
187 JevDeadline.check();
188 if(!synchronous)poll();
189 // A fresh round requires a >=3-member action. Smaller groups can only
190 // supply already accepted pending work. Check that necessary condition
191 // before rendering identities or walking exact-execution history.
192 long eligibilityStart=JevReviewProfile.start();
193 boolean workPossible=hasReviewWork(state,tree);
194 JevReviewProfile.end("review_eligibility",eligibilityStart);
195 if(!workPossible){JevReviewProfile.add("return_no_review_work",0,1);return;}
196 long scanStart=JevReviewProfile.start();
197 Map<String,Candidate> lightweight=collectCandidates(state,tree);
198 JevReviewProfile.end("candidate_enumeration",scanStart);
199 if(lightweight.isEmpty()){JevReviewProfile.add("return_no_candidates",0,1);return;}
200 long selectStart=JevReviewProfile.start();
201 Set<String> roundKeys=selectRoundKeys(state,tree,lightweight);
202 JevReviewProfile.end("round_selection",selectStart);
203 if(roundKeys.isEmpty()){JevReviewProfile.add("return_no_round",0,1);return;}
204 Map<String,Candidate> reusable=new LinkedHashMap<String,Candidate>();
205 for(Candidate candidate:lightweight.values())if(roundKeys.contains(candidate.key)&&candidate.staged!=null
206 && "PROBE".equals(candidate.staged.decision.outcome))reusable.put(candidate.key,candidate);
207 if(!reusable.isEmpty()) {
208 preparedPage=new Page(state.getGraphId()+"|staged",new JSONObject(),reusable);
209 preparedState=state;preparedTree=tree;
210 JevReviewProfile.add("return_staged_probe",0,1);
211 return;
212 }
213 Page page;long evidenceStart=System.nanoTime();
214 long pairStart=JevReviewProfile.start();
215 try {page=roundPage(state,tree,roundKeys,lightweight);}finally{localEvidenceNanos+=System.nanoTime()-evidenceStart;JevReviewProfile.end("round_page",pairStart);}
216 preparedPage=page;preparedState=state;preparedTree=tree;
217 if(page==null||page.candidates.isEmpty()||page.payload.optJSONArray("pairs").length()==0)return;
218 // A staged PROMOTE is an executable piece of work. Do not spend this
219 // review window constructing/submitting a new model request when the
220 // current GUI can still execute one of the old probes.
221 int retries=synchronous?resumeRetained(state,tree,page,deadline):0;
222 if(retries<0)return;
223 Map<String,Candidate> pending=new LinkedHashMap<String,Candidate>();
224 String currentGui=JevEvidence.guiIdentity(tree);
225 for(Map.Entry<String,Candidate> entry:page.candidates.entrySet()) {
226 Candidate candidate=entry.getValue();
227 JevReviewLedger.Staged prior=staged(candidate,currentGui,state);
228 if(prior==null && !inFlight.contains(entry.getKey()) && page.payload.optJSONObject("coverage").optJSONObject(candidate.candidateId).optInt("selected_history")>0)pending.put(entry.getKey(),candidate);
229 }
230 if(pending.isEmpty())return;
231 try {
232 Page requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,pending),pending);
233 if(requestPage.payload.getJSONArray("pairs").length()==0)return;
234 Cache cache=pages.get(requestPage.cacheKey);
235 if(cache!=null)return;
236 JevHistoryPairwiseProtocol.Prepared prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes);
237 JevHistoryPairwiseProtocol.Request fullRequest=prepared.request("semantic-cache-"+sequence.incrementAndGet(),requestPage.payload);
238 Map<String,JevReviewLedger.PairResult> pairHits=new LinkedHashMap<String,JevReviewLedger.PairResult>();
239 for(int i=0;i<requestPage.payload.getJSONArray("pairs").length();i++) {
240 JSONObject pair=requestPage.payload.getJSONArray("pairs").getJSONObject(i);
241 String key=JevHistoryPairwiseProtocol.pairFingerprint(fullRequest,pair);
242 Map<String,JevReviewLedger.PairResult> hit=registry.reviewLedger().pairResults(Collections.singleton(key));
243 if(hit.isEmpty()){pairHits.clear();break;}
244 pairHits.put(key,hit.get(key));
245 }
246 if(!pairHits.isEmpty() && pairHits.size()==requestPage.payload.getJSONArray("pairs").length()) {
247 JSONObject answers=new JSONObject();JSONArray pairs=requestPage.payload.getJSONArray("pairs");
248 for(int i=0;i<pairs.length();i++) {
249 JSONObject pair=pairs.getJSONObject(i);String key=JevHistoryPairwiseProtocol.pairFingerprint(fullRequest,pair);
250 answers.put(pair.getString("id"),pairHits.get(key).json());
251 }
252 Cache cached=new Cache(requestPage.candidates,fullRequest,null,0,JevEvidence.guiIdentity(tree));
253 Candidate firstCandidate=requestPage.candidates.values().iterator().next();
254 cached.sourceVersion=modelVersion;cached.historyRevision=registry.executionRevision(state,firstCandidate.action);cached.confirmationCount=registry.confirmationCount();
255 pages.put(requestPage.cacheKey,cached);inFlight.addAll(requestPage.candidates.keySet());cached.pairCacheHit=true;
256 for(Candidate candidate:requestPage.candidates.values())bindSource(candidate,cached.sourceGui,cached.historyRevision);
257 consumeCache(requestPage.cacheKey,cached,JevTransport.Result.cached(new JSONObject().put("answers",answers).toString()));
258 return;
259 }
260 Map<ConcreteActionIdentity,JevReviewLedger.Decision> fingerprintHits=
261 registry.reviewLedger().byFingerprint(requestPage.payload.optString("evidence_view_version").isEmpty()?contract:contract+":"+requestPage.payload.optString("evidence_view_version"),scopeForPage(pending),prepared.inputFingerprint);
262 Map<String,Candidate> fingerprintMisses=new LinkedHashMap<String,Candidate>();
263 for(Map.Entry<String,Candidate> entry:pending.entrySet())
264 if(!fingerprintHits.containsKey(identity(entry.getValue())))fingerprintMisses.put(entry.getKey(),entry.getValue());
265 if(fingerprintMisses.isEmpty())return;
266 if(fingerprintMisses.size()!=pending.size()) {
267 requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,fingerprintMisses),fingerprintMisses);
268 if(requestPage.payload.getJSONArray("pairs").length()==0)return;
269 prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes);
270 }
271 JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),requestPage.payload);
272 while(true) {
273 long remaining=deadline-System.nanoTime();
274 if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) {
275 failOpen(state,remaining<=0?"DEADLINE":"INTERRUPTED");return;
276 }
277 JevDeadline.check();
278 int attempt=nextAttempt(request.inputFingerprint);
279 // Capture before submitting, so expiry cannot orphan an accepted call.
280 String sourceGui=JevEvidence.guiIdentity(tree);
281 long historyRevision=registry.executionRevision(state,requestPage.candidates.values().iterator().next().action);
282 receipt("semantic-request-"+request.requestId,requestReceipt(request,requestPage.candidates,attempt));
283 registry.semanticEvent("JEV_REQUEST_SENT",currentStep,state.getGraphId(),null,JevRegistry.fields(
284 "request_id",request.requestId,"review_id",reviewId,"candidate_count",requestPage.candidates.size(),"pair_count",request.questionCount,"request_bytes",request.requestBytes,"wait_mode",synchronous?"synchronous":"asynchronous",
285 "attempt",attempt,"input_fingerprint",request.inputFingerprint));
286 JevDeadline.check();
287 JevTransport.Call call=synchronous
288 ?transport.submitRetained(request.requestId,request.bodyJson,requestLifetimeMs,Math.max(16384,maxBytes))
289 :transport.submit(request.requestId,request.bodyJson,timeoutMs,Math.max(16384,maxBytes));
290 if(call==null) {
291 requestFailure(request,attempt,"QUEUE_FULL",0,null);
292 if(retries++<MAX_RETRIES){
293 request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes);continue;
294 }
295 failOpen(state,"QUEUE_FULL");return;
296 }
297 Cache submitted=new Cache(requestPage.candidates,request,call,attempt,sourceGui);
298 submitted.sourceVersion=modelVersion;submitted.historyRevision=historyRevision;
299 submitted.confirmationCount=registry.confirmationCount();
300 pages.put(requestPage.cacheKey,submitted);
301 if(synchronous)archiveOnArrival(submitted,currentStep);
302 inFlight.addAll(requestPage.candidates.keySet());
303 for(Candidate candidate:pending.values())bindSource(candidate,sourceGui,historyRevision);
304 if(!synchronous)return;
305 registry.semanticEvent("SEMANTIC_WAIT_STARTED",currentStep,state.getGraphId(),null,JevRegistry.fields(
306 "request_id",request.requestId,"candidate_count",requestPage.candidates.size(),"attempt",attempt));
307 JevTransport.Result response=await(call,deadline);
308 JevDeadline.check();
309 if(response==null)throw new JevDeadline.Expired();
310 boolean completed=consumeCache(requestPage.cacheKey,submitted,response);
311 if(completed)return;
312 if(retries++>=MAX_RETRIES){failOpen(state,"RETRIES_EXHAUSTED");return;}
313 request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes);
314 }
315 } catch(JSONException ignored) {
316 if("SEMANTIC_REQUEST_BUDGET_EXCEEDED".equals(ignored.getMessage())) {
317 if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) {
318 try {
319 if(submitProjected(state,tree,page,pending,deadline,retries))return;
320 } catch(JSONException projectedError) {
321 healthy=false;localFailure=true;selectionBlocked=true;
322 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields(
323 "reason","REQUEST_BUILD_FAILED","error",projectedError.getMessage(),
324 "evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION));
325 return;
326 }
327 }
328 selectionBlocked=true;
329 JSONObject fields=JevRegistry.fields("reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId);
330 if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) {
331 JevHistoryPairwiseProtocol.BudgetExceeded size=(JevHistoryPairwiseProtocol.BudgetExceeded)ignored;
332 JevRegistry.merge(fields,JevRegistry.fields("request_bytes",size.requestBytes,"state_bytes",size.stateBytes,
333 "questions_bytes",size.questionsBytes,"candidate_count",size.candidateCount,"pair_count",size.pairCount,"limit_bytes",size.limitBytes));
334 }
335 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,fields);
336 } else {
337 healthy=false;localFailure=true;selectionBlocked=true;
338 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields("reason","REQUEST_BUILD_FAILED","error",ignored.getMessage()));
339 }
340 }
341 }
342 private boolean hasReviewWork(State state,GUITree tree) {
343 for(ModelAction action:state.getActions()) {
344 JevDeadline.check();
345 if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue;
346 GUITreeNode[] nodes=action.getResolvedNodes();
347 if(nodes!=null&&nodes.length>=MIN_CONCRETE_ACTIONS&&action.getResolvedGUITree()==tree)return true;
348 }
349 // Do not apply the fresh-round size threshold to persistent Probes.
350 return registry.reviewLedger().hasPendingProbe(contract,state);
351 }
352 private Map<String,Candidate> collectCandidates(State state,GUITree tree) {
353 Map<String,Candidate> candidates=new LinkedHashMap<String,Candidate>();Set<String> ambiguous=new HashSet<String>();int number=0;
354 for(ModelAction action:state.getActions()) {
355 JevDeadline.check();
356 if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue;
357 GUITreeNode[] nodes=action.getResolvedNodes();
358 if(nodes==null||nodes.length==0||action.getResolvedGUITree()!=tree)continue;
359 for(GUITreeNode node:nodes) {
360 JevDeadline.check();if(registry.hasExactExecution(state,action,tree,node))continue;
361 ConcreteActionIdentity concrete=ConcreteActionIdentity.of(action.getType().name(),node);
362 String id="c"+(++number),identity=scope(action)+"|"+concrete.toString();
363 if(ambiguous.contains(identity))continue;
364 if(candidates.containsKey(identity)){candidates.remove(identity);ambiguous.add(identity);}
365 else {Candidate c=new Candidate(action,node,identity,id);c.concreteIdentity=concrete;candidates.put(identity,c);}
366 }
367 }
368 return candidates;
369 }
370 private JevReviewLedger.Staged staged(Candidate c,String gui,State state){
371 long start=JevReviewProfile.start();
372 try {return registry.reviewLedger().staged(contract,state,c.action,identity(c),gui,modelVersion,registry.executionRevision(state,c.action),registry.confirmationCount());}
373 finally {JevReviewProfile.end("staged_lookup",start);}
374 }
375 private boolean executable(Candidate c,State state,GUITree tree){
376 if(c.staged==null||!"PROBE".equals(c.staged.decision.outcome)||!c.node.isEnabled()
377 ||!c.action.isEnabled()||!c.action.isValid()||c.action.getState()!=state||tree.getCurrentState()!=state)return false;
378 if(JevRegistry.correspondingNode(c.staged.source,tree,c.action)!=c.node || !navigationMask.allow(state,tree,c))return false;
379 if(nativeValidator==null)return true;
380 ModelAction action=c.action;GUITreeNode previous=action.getResolvedNode();
381 com.android.commands.monkey.ape.tree.GUITreeAction previousAction=action.getResolvedGUITreeAction();
382 int throttle=action.getThrottle();
383 try {action.resolveAt((int)currentStep,throttle,tree,c.node,action.getResolvedNodes());return nativeValidator.valid(action);}
384 finally {action.resolveAt((int)currentStep,throttle,tree,previous,action.getResolvedNodes());action.setResolvedGUITreeAction(previousAction);}
385 }
386 private Set<String> selectRoundKeys(State state,GUITree tree,Map<String,Candidate> all) {
387 long guiStart=JevReviewProfile.start();String gui=JevEvidence.guiIdentity(tree);JevReviewProfile.end("round_gui_identity",guiStart);
388 List<Candidate> reusable=new ArrayList<Candidate>(),fresh=new ArrayList<Candidate>();
389 for(Candidate c:all.values()){
390 c.staged=staged(c,gui,state);
391 if(c.staged!=null){
392 c.evidenceScope=c.staged.scope;c.reviewContract=c.staged.contract;
393 if(executable(c,state,tree))reusable.add(c);
394 } else if(c.node.isEnabled()&&c.action.getResolvedNodes().length>=MIN_CONCRETE_ACTIONS)fresh.add(c);
395 }
396 Set<String> out=new LinkedHashSet<String>();
397 if(!reusable.isEmpty()){for(Candidate c:reusable)out.add(c.key);return out;}
398 if(fresh.isEmpty())return out;
399 String cursorKey=state.getGraphId()+"|"+scope(fresh.get(0).action);
400 int offset=incrementalOffsets.containsKey(cursorKey)?incrementalOffsets.get(cursorKey):0;
401 offset=((offset%fresh.size())+fresh.size())%fresh.size();
402 // Small pages retain their established behavior; large pages never
403 // materialize a half-page only to discard most of it at byte checking.
404 int count=Math.min(MAX_CANDIDATES_PER_ROUND,fresh.size()<=3?fresh.size():(fresh.size()+1)/2);
405 for(int i=0;i<count;i++)out.add(fresh.get((offset+i)%fresh.size()).key);
406 incrementalOffsets.put(cursorKey,offset+count);return out;
407 }
408 private JevReviewLedger.Decision reusableDecision(Candidate c,String gui,State state) {
409 JevReviewLedger.Staged value=staged(c,gui,state);
410 if(value==null||!"PROBE".equals(value.decision.outcome))return null;
411 c.staged=value;c.evidenceScope=value.scope;c.reviewContract=value.contract;return value.decision;
412 }
413 private void bindSource(Candidate c,String gui,long revision){
414 registry.reviewLedger().bindSource(c.reviewContract==null?contract:c.reviewContract,scope(c),identity(c),gui,
415 modelVersion,revision,registry.confirmationCount(),new com.android.commands.monkey.ape.tree.GUITreeAction(c.action.getResolvedGUITree(),c.node,c.action));
416 }
417 /**
418 * Fit an already bounded round to one complete candidate-closure request.
419 * Top-3 and coverage/provenance come from the frozen round plan; this
420 * method never ranks a projection or expands the remaining page inventory.
421 */
422 private boolean submitProjected(State state,GUITree tree,Page page,Map<String,Candidate> pending,long deadline,int retries)throws JSONException {
423 JevPairwiseEvidenceView.Plan projection=new JevPairwiseEvidenceView.Plan(page.payload);
424 List<Candidate> ordered=new ArrayList<Candidate>();
425 for(Candidate candidate:pending.values())
426 if(page.payload.getJSONObject("coverage").getJSONObject(candidate.candidateId).getInt("selected_history")>0)ordered.add(candidate);
427 int offset=0;
428 while(offset<ordered.size()) {
429 LinkedHashMap<String,Candidate> batch=new LinkedHashMap<String,Candidate>();
430 JevHistoryPairwiseProtocol.Prepared prepared=null;JSONObject projected=null;
431 while(offset<ordered.size()) {
432 JevDeadline.check();Candidate next=ordered.get(offset);batch.put(next.key,next);
433 List<String> selectedIds=new ArrayList<String>();
434 for(Candidate c:batch.values())selectedIds.add(c.candidateId);
435 long projectStart=JevReviewProfile.start();JSONObject trial=projection.project(selectedIds);
436 JevReviewProfile.end("closure_projection",projectStart);
437 try {
438 JevHistoryPairwiseProtocol.Prepared candidatePrepared=JevHistoryPairwiseProtocol.prepare(model,trial,maxBytes);
439 prepared=candidatePrepared;projected=trial;offset++;
440 } catch(JevHistoryPairwiseProtocol.BudgetExceeded tooLarge) {
441 batch.remove(next.key);
442 if(!batch.isEmpty())break;
443 // An indivisible candidate + its complete original Top-3
444 // cannot fit. Skip that closure; do not discard evidence.
445 budgetFailure(state,tooLarge,next.candidateId,JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION);
446 offset++;
447 }
448 }
449 if(batch.isEmpty())continue;
450 if(prepared==null || projected==null)throw new JSONException("PROJECTED_BATCH_EMPTY");
451 for(Candidate candidate:batch.values()) {
452 candidate.reviewContract=projectedContract();
453 candidate.evidenceScope=scope(candidate.action)+"|"+prepared.inputSignature;
454 }
455 Map<ConcreteActionIdentity,JevReviewLedger.Decision> hits=registry.reviewLedger().byFingerprint(
456 projectedContract(),scopeForPage(batch),prepared.inputFingerprint);
457 boolean allCached=true;
458 for(Candidate candidate:batch.values())if(!hits.containsKey(identity(candidate))){allCached=false;break;}
459 if(allCached)continue; // no Request.plan deep copy on a view-local cache hit
460 String cacheKey=page.cacheKey+"|"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION+"|"+prepared.inputFingerprint;
461 if(pages.containsKey(cacheKey))continue;
462 JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);
463 while(true) {
464 long remaining=deadline-System.nanoTime();
465 if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) {failOpen(state,"DEADLINE");return true;}
466 JevDeadline.check();int attempt=nextAttempt(request.inputFingerprint);
467 String sourceGui=JevEvidence.guiIdentity(tree);long historyRevision=registry.executionRevision(state,batch.values().iterator().next().action);
468 receipt("semantic-request-"+request.requestId,requestReceipt(request,batch,attempt));
469 registry.semanticEvent("JEV_REQUEST_SENT",currentStep,state.getGraphId(),null,JevRegistry.fields(
470 "request_id",request.requestId,"review_id",reviewId,"candidate_count",batch.size(),"pair_count",request.questionCount,
471 "request_bytes",request.requestBytes,"wait_mode",synchronous?"synchronous":"asynchronous","attempt",attempt,
472 "input_fingerprint",request.inputFingerprint,"evidence_view_version",request.evidenceViewVersion));
473 JevDeadline.check();
474 JevTransport.Call call=synchronous
475 ?transport.submitRetained(request.requestId,request.bodyJson,requestLifetimeMs,Math.max(16384,maxBytes))
476 :transport.submit(request.requestId,request.bodyJson,timeoutMs,Math.max(16384,maxBytes));
477 if(call==null) {
478 requestFailure(request,attempt,"QUEUE_FULL",0,null);
479 if(retries++<MAX_RETRIES){request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);continue;}
480 failOpen(state,"QUEUE_FULL");return true;
481 }
482 Cache submitted=new Cache(batch,request,call,attempt,sourceGui);
483 submitted.sourceVersion=modelVersion;submitted.historyRevision=historyRevision;submitted.confirmationCount=registry.confirmationCount();
484 pages.put(cacheKey,submitted);if(synchronous)archiveOnArrival(submitted,currentStep);
485 inFlight.addAll(batch.keySet());
486 for(Candidate candidate:batch.values())bindSource(candidate,sourceGui,historyRevision);
487 if(!synchronous)break;
488 registry.semanticEvent("SEMANTIC_WAIT_STARTED",currentStep,state.getGraphId(),null,JevRegistry.fields(
489 "request_id",request.requestId,"candidate_count",batch.size(),"attempt",attempt,
490 "evidence_view_version",request.evidenceViewVersion));
491 JevTransport.Result response=await(call,deadline);JevDeadline.check();
492 if(response==null)throw new JevDeadline.Expired();
493 if(consumeCache(cacheKey,submitted,response))break;
494 if(retries++>=MAX_RETRIES){failOpen(state,"RETRIES_EXHAUSTED");return true;}
495 request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);
496 }
497 // One bounded logical request per Probe entry. Remaining eligible
498 // candidates stay unreviewed and are selected by the next round's
499 // rotating cursor; this avoids turning a large page into an
500 // implicit packet fan-out.
501 return true;
502 }
503 return true;
504 }
505 private void budgetFailure(State state,JevHistoryPairwiseProtocol.BudgetExceeded size,String candidateId,String version) {
506 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields(
507 "reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId,
508 "candidate_id",candidateId,"evidence_view_version",version,"request_bytes",size.requestBytes,
509 "state_bytes",size.stateBytes,"questions_bytes",size.questionsBytes,"candidate_count",size.candidateCount,
510 "pair_count",size.pairCount,"limit_bytes",size.limitBytes));
511 }
512 private JevTransport.Result await(JevTransport.Call call,long deadline) {
513 long began=System.nanoTime();
514 try{return call.awaitUntil(deadline);}finally{providerWaitNanos+=System.nanoTime()-began;}
515 }
516 private void failOpen(State state,String reason) {
517 healthy=false;selectionBlocked=true;
518 registry.semanticEvent("SEMANTIC_FAIL_OPEN",currentStep,state.getGraphId(),null,
519 JevRegistry.fields("reason",reason,"fallback","native","semantic_selection_blocked",true));
520 }
521 private void retain(Cache cache) {
522 retained.put(cache.request.requestId,cache);
523 while(retained.size()>retainedCapacity) {
524 Iterator<Map.Entry<String,Cache>> it=retained.entrySet().iterator();
525 Cache evicted=it.next().getValue();it.remove();evicted.call.cancel("RETAINED_CAPACITY");
526 }
527 }
528 /** Archive raw bytes even if APE never revisits. No decisions are parsed or applied here. */
529 private void archiveOnArrival(Cache cache,final long sourceStep) {
530 if(receiptSink==null)return;
531 final JevHistoryPairwiseProtocol.Request request=cache.request;
532 final long sourceVersion=cache.sourceVersion,historyRevision=cache.historyRevision;
533 final String sourceGui=cache.sourceGui;
534 cache.call.onComplete(new JevTransport.Completion(){public void completed(final JevTransport.Result result) {
535 try {receiptWorker.execute(new Runnable(){public void run() {
536 try {
537 JSONObject value=new JSONObject().put("schema",1).put("kind","semantic_provider_arrival")
538 .put("request_id",request.requestId).put("endpoint",endpoint).put("model",model)
539 .put("contract",requestContract(request)).put("source_step",sourceStep).put("source_version",sourceVersion)
540 .put("history_revision",historyRevision).put("source_gui",sourceGui)
541 .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint)
542 .put("http_status",result.httpStatus).put("error",result.error==null?JSONObject.NULL:result.error)
543 .put("raw_response",result.raw==null?JSONObject.NULL:result.raw)
544 .put("submitted_ns",result.submittedNanos).put("sent_ns",result.sentNanos)
545 .put("completed_ns",result.completedNanos).put("wait_ns",result.waitNanos())
546 .put("validated",false).put("applied",false);
547 viewMetadata(value,request);
548 if(!receiptSink.save("semantic-arrival-"+request.requestId,value))healthy=false;
549 }catch(JSONException e){healthy=false;}catch(RuntimeException e){healthy=false;}
550 }});}catch(RejectedExecutionException e){healthy=false;}
551 }});
552 }
553 /** A later review may reuse raw work only against the same visible input and history. */
554 private int resumeRetained(State state,GUITree tree,Page page,long deadline) {
555 for(Map.Entry<String,Cache> item:new ArrayList<Map.Entry<String,Cache>>(retained.entrySet())) {
556 JevDeadline.check();Cache old=item.getValue();
557 if(old.sourceVersion!=modelVersion || old.sourceState!=state) {
558 old.call.cancel("STALE_HISTORY_OR_VERSION");retained.remove(item.getKey());continue;
559 }
560 JevTransport.Result response=old.call.poll();
561 String expectedInput=JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION.equals(old.request.evidenceViewVersion)?old.request.inputSignature:old.request.sourceInputSignature;
562 if(!expectedInput.equals(page.payload.optString("input_signature"))) {
563 if(response!=null && response.error!=null)retained.remove(item.getKey());
564 continue;
565 }
566 if(old.historyRevision!=registry.executionRevision(state,old.candidates.values().iterator().next().action)) {
567 old.call.cancel("STALE_HISTORY_OR_VERSION");retained.remove(item.getKey());continue;
568 }
569 if(!old.sourceGui.equals(JevEvidence.guiIdentity(tree)))continue;
570 Map<String,Candidate> current=new LinkedHashMap<String,Candidate>();
571 for(String key:old.candidates.keySet()) {
572 Candidate c=page.candidates.get(key);
573 if(c!=null && old.byId.containsKey(c.candidateId) && old.byId.get(c.candidateId).key.equals(key))current.put(key,c);
574 }
575 if(current.size()!=old.request.candidateIds.size()) {
576 old.call.cancel("STALE_CANDIDATES");retained.remove(item.getKey());continue;
577 }
578 if(!old.request.evidenceViewVersion.isEmpty())for(Candidate candidate:current.values()) {
579 candidate.evidenceScope=scope(candidate.action)+"|"+old.request.inputSignature;
580 candidate.reviewContract=effectiveContract(old);
581 }
582 Cache rebound=new Cache(current,old.request,old.call,old.attempt,JevEvidence.guiIdentity(tree));
583 rebound.sourceVersion=old.sourceVersion;rebound.historyRevision=old.historyRevision;
584 rebound.confirmationCount=old.confirmationCount;
585 // Keep it recoverable even if parsing/receipt processing exhausts this later budget.
586 pages.put(item.getKey(),rebound);retained.remove(item.getKey());
587 if(response==null)response=await(old.call,deadline);
588 JevDeadline.check();
589 if(response==null)throw new JevDeadline.Expired();
590 if(consumeCache(item.getKey(),rebound,response)) {
591 if(old.request.evidenceViewVersion.isEmpty())return -1;
592 continue;
593 }
594 // Protocol / HTTP failures remain failures, rather than reusable semantic answers.
595 return 1;
596 }
597 return 0;
598 }
599 private static final class Page {
600 final String cacheKey;final JSONObject payload;final Map<String,Candidate> candidates;
601 Page(String k,JSONObject p,Map<String,Candidate> c){cacheKey=k;payload=p;candidates=c;}
602 }
603 private Page page(final State state,final GUITree tree) { return page(state,tree,null); }
604 private Page page(final State state,final GUITree tree,final Set<String> include) {
605 return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<Page>() {
606 public Page get(){try(JevRegistry.ConcreteLookup lookup=new JevRegistry.ConcreteLookup()){return buildPage(state,tree,include);}}
607 });
608 }
609 private Page roundPage(final State state,final GUITree tree,final Set<String> include,final Map<String,Candidate> descriptors){
610 return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<Page>(){
611 public Page get(){return buildPage(state,tree,include,descriptors);}
612 });
613 }
614 private Page buildPage(State state,GUITree tree,Set<String> include){return buildPage(state,tree,include,null);}
615 private Page buildPage(State state,GUITree tree,Set<String> include,Map<String,Candidate> descriptors) {
616 long began=System.nanoTime();
617 try {
618 JevRegistry.ConfirmedHistory history=registry.confirmedHistory(state);
619 long evidenceStart=System.nanoTime();
620 Map<String,Candidate> candidates=new LinkedHashMap<String,Candidate>();Set<String> ambiguous=new HashSet<String>();int number=0;
621 if(descriptors!=null){for(Candidate c:descriptors.values())if(include.contains(c.key))candidates.put(c.key,c);}
622 else for(ModelAction action:state.getActions()) {
623 JevDeadline.check();
624 if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue;
625 GUITreeNode[] nodes=action.getResolvedNodes();
626 if(nodes==null||nodes.length<MIN_CONCRETE_ACTIONS||action.getResolvedGUITree()!=tree)continue;
627 for(GUITreeNode node:nodes) {
628 JevDeadline.check();if(registry.hasExactExecution(state,action,tree,node))continue;
629 String id="c"+(++number),identity=key(action,node);
630 if(include!=null&&!include.contains(identity))continue;
631 if(ambiguous.contains(identity))continue;
632 if(candidates.containsKey(identity)){candidates.remove(identity);ambiguous.add(identity);}
633 else {Candidate c=new Candidate(action,node,identity,id);c.concreteIdentity=ConcreteActionIdentity.of(action.getType().name(),node);candidates.put(identity,c);}
634 }
635 }
636 if(candidates.isEmpty())return null;
637 JSONObject pages=new JSONObject(),current=new JSONObject(),executed=new JSONObject(),coverage=new JSONObject(),provenance=new JSONObject();
638 JSONArray pairs=new JSONArray(),ids=new JSONArray();
639 JSONObject recoverable=new JSONObject();Set<String> usedHistory=new java.util.LinkedHashSet<String>(),usedPages=new HashSet<String>();
640 IdentityHashMap<GUITree,String> pageIds=new IdentityHashMap<GUITree,String>();
641 IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,String> historyIds=new IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,String>();
642 IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,JevEvidence.PairDescriptor> historyDescriptors=new IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,JevEvidence.PairDescriptor>();
643 Map<String,com.android.commands.monkey.ape.tree.GUITreeAction> historyById=new LinkedHashMap<String,com.android.commands.monkey.ape.tree.GUITreeAction>();
644 int recoverableHistoryCount=0;
645 for(Candidate c:candidates.values()) {
646 JevDeadline.check();ids.put(c.candidateId);c.auditSource=tree;
647 String pageId=sourcePage(tree,pages,pageIds);
648 long descriptorStartCurrent=JevReviewProfile.start();
649 JevEvidence.PairDescriptor candidateDescriptor=descriptors==null?null:JevEvidence.pairDescriptor(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action));
650 JevReviewProfile.end("candidate_descriptor",descriptorStartCurrent);
651 long richStart=JevReviewProfile.start();
652 JSONObject control=descriptors==null?JevEvidence.pairAction(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action),pageId):null;
653 if(descriptors==null)JevReviewProfile.end("candidate_rich",richStart);
654 if(control!=null)current.put(c.candidateId,control);
655 List<com.android.commands.monkey.ape.tree.GUITreeAction> members=history.members(c.action);
656 Map<String,Set<String>> featurePool=new LinkedHashMap<String,Set<String>>();
657 for(com.android.commands.monkey.ape.tree.GUITreeAction h:members) {
658 JevDeadline.check();String hid=historyIds.get(h);
659 if(hid==null) {
660 hid="h"+(historyIds.size()+1);historyIds.put(h,hid);
661 historyById.put(hid,h);
662 sourcePage(h.getGUITree(),pages,pageIds); // preserve old Page ID allocation order
663 long descriptorStart=JevReviewProfile.start();
664 JevEvidence.PairDescriptor descriptor=JevEvidence.pairDescriptor(h);
665 JevReviewProfile.end("history_descriptor",descriptorStart);
666 historyDescriptors.put(h,descriptor);
667 if(descriptor.recoverable)recoverableHistoryCount++;
668 provenance.put(hid,new JSONObject().put("source_tree",h.getGUITree().getId())
669 .put("source_gui_timestamp",h.getGUITree().getTimestamp()).put("current_state",state.getGraphId())
670 .put("current_model_action",h.getModelAction().getGraphId()).put("current_version",modelVersion)
671 .put("input_confirmed",true).put("confirmation_basis","live confirmedInputs before review; not function completion")
672 .put("attributes_recoverable",descriptor.recoverable));
673 }
674 JevEvidence.PairDescriptor descriptor=historyDescriptors.get(h);
675 if(descriptor!=null && descriptor.recoverable)featurePool.put(hid,descriptor.selectionFeatures);
676 }
677 long rankStart=JevReviewProfile.start();
678 List<String> selected=descriptors==null?(control==null?Collections.<String>emptyList():JevHistoryPairwiseProtocol.selectHistory(control,featurePool.keySet(),featurePool))
679 :(!candidateDescriptor.recoverable?Collections.<String>emptyList():JevHistoryPairwiseProtocol.selectHistoryFeatures(candidateDescriptor.selectionFeatures,featurePool.keySet(),featurePool));
680 JevReviewProfile.end("history_ranking",rankStart);
681 if(descriptors!=null && !selected.isEmpty()){
682 long currentRich=JevReviewProfile.start();control=JevEvidence.pairAction(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action),pageId);
683 JevReviewProfile.end("candidate_rich",currentRich);if(control==null)throw new IllegalStateException("selected candidate became unrecoverable");
684 current.put(c.candidateId,control);
685 }
686 long pairStart=JevReviewProfile.start();JSONArray selectedIds=new JSONArray();
687 for(String hid:selected){
688 selectedIds.put(hid);usedHistory.add(hid);c.auditHistory.put(hid,historyById.get(hid));
689 pairs.put(new JSONObject().put("id","p"+(pairs.length()+1)).put("candidate",c.candidateId).put("history",hid));
690 }
691 JevReviewProfile.end("pair_generation",pairStart);
692 coverage.put(c.candidateId,new JSONObject().put("legal_history_count",members.size()).put("complete",history.complete && featurePool.size()==members.size())
693 .put("history_collection_complete",history.complete).put("recoverable_history_count",featurePool.size()).put("selected_history",selected.size()).put("selected_history_ids",selectedIds)
694 .put("unknown_mapping_count",history.unknown)
695 .put("mapping_basis","live confirmedInputs; current State/ModelAction object links and node Name")
696 .put("context_recoverable",descriptors==null?control!=null:candidateDescriptor.recoverable));
697 }
698 // Only the selected history witnesses receive the expensive rich JSON form.
699 // Descriptor extraction above has already frozen the exact ranking inputs.
700 for(String hid:usedHistory) {
701 com.android.commands.monkey.ape.tree.GUITreeAction h=historyById.get(hid);
702 if(h==null)throw new IllegalStateException("missing selected history descriptor: "+hid);
703 long witnessStart=JevReviewProfile.start();
704 JSONObject witness=JevEvidence.pairAction(h,pageIds.get(h.getGUITree()));
705 JevReviewProfile.end("history_rich",witnessStart);
706 if(witness==null)throw new IllegalStateException("selected history became unrecoverable: "+hid);
707 recoverable.put(hid,witness);
708 }
709 // Send source context only for current candidates and selected witnesses, once per source page.
710 for(Iterator<?> it=current.keys();it.hasNext();)usedPages.add(current.getJSONObject((String)it.next()).getString("source_page_id"));
711 for(String hid:usedHistory){JSONObject witness=recoverable.getJSONObject(hid);executed.put(hid,witness);usedPages.add(witness.getString("source_page_id"));}
712 List<String> discard=new ArrayList<String>();for(Iterator<?> it=pages.keys();it.hasNext();){String id=(String)it.next();if(!usedPages.contains(id))discard.add(id);}
713 for(String id:discard)pages.remove(id);
714 long stateStart=JevReviewProfile.start();
715 JSONObject stateEvidence=new JSONObject().put("evidence_schema",JevHistoryPairwiseProtocol.VERSION)
716 .put("pages",pages).put("candidates",current).put("executed_actions",executed);
717 JSONObject payload=new JSONObject().put("state",stateEvidence).put("pairs",pairs).put("candidate_ids",ids).put("coverage",coverage)
718 .put("history_provenance",provenance).put("aggregation_policy",JevHistoryPairwiseProtocol.POLICY)
719 .put("review_contract",contract)
720 .put("provider_endpoint",endpoint)
721 .put("model_version",modelVersion).put("state_id",state.getGraphId())
722 .put("confirmation_count",history.total).put("unknown_mapping_count",history.unknown)
723 .put("execution_revision",registry.executionRevision(state));
724 if(descriptors!=null){
725 JSONArray overview=new JSONArray();
726 for(Candidate c:descriptors.values())if(!current.has(c.candidateId))overview.put(new JSONArray().put(c.candidateId).put(c.action.getType().name())
727 .put(JevEvidence.boundedText(c.node.getClassName(),64)).put(JevEvidence.boundedText(c.node.getResourceID(),64))
728 .put(JevEvidence.boundedText(c.node.getText(),64)).put(c.node.isEnabled()));
729 incrementalView(payload,overview);
730 }
731 JevReviewProfile.end("state_projection",stateStart);
732 long jsonStart=JevReviewProfile.start();
733 String serialized=payload.toString();
734 String signature=JevEvidence.digest(serialized);payload.put("input_signature",signature);
735 JevReviewProfile.end("plan_serialize_signature",jsonStart);
736 JevReviewProfile.add("serialized_plan_bytes",0,serialized.getBytes(JevHistoryPairwiseProtocol.UTF8).length);
737 for(Candidate c:candidates.values()){c.evidenceScope=scope(c.action)+"|"+signature;c.reviewContract=descriptors==null?contract:contract+":"+JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION;}
738 registry.semanticEvent("HISTORY_PAIRWISE_COST",currentStep,state.getGraphId(),null,JevRegistry.fields(
739 "history_enumeration_ns",history.enumerationNanos,"evidence_build_ns",System.nanoTime()-evidenceStart,
740 "prepare_local_ns",System.nanoTime()-began,"confirmed_input_objects",history.total,"unknown_mapping_count",history.unknown,
741 "history_complete",history.complete,"recoverable_history_count",recoverableHistoryCount,"selected_history",executed.length(),"candidate_count",candidates.size(),"pair_count",pairs.length(),
742 "evidence_bytes",stateEvidence.toString().getBytes(JevHistoryPairwiseProtocol.UTF8).length));
743 return new Page(state.getGraphId()+"|"+signature,payload,candidates);
744 }catch(JSONException error){throw new IllegalStateException("pair evidence construction failed",error);}
745 }
746 /** Shared wire contract for live descriptors and archived-evidence regression. */
747 static void incrementalView(JSONObject payload,JSONArray overview)throws JSONException {
748 payload.getJSONObject("state").put("evidence_schema",JevHistoryPairwiseProtocol.VERSION+":"+JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION)
749 .put("page_operation_context",new JSONObject().put("columns",new JSONArray().put("id").put("action_type").put("class").put("resource_id").put("text_max64").put("enabled"))
750 .put("actions",overview).put("view","lightweight inventory; target subtree/parent/local context only for compared candidates"));
751 payload.put("evidence_view_version",JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION);
752 payload.put("source_input_signature",JevEvidence.digest(payload.toString()));
753 }
754 private static String sourcePage(GUITree tree,JSONObject pages,IdentityHashMap<GUITree,String> pageIds)throws JSONException {
755 String id=pageIds.get(tree);
756 if(id==null){id="page"+(pageIds.size()+1);pageIds.put(tree,id);
757 pages.put(id,new JSONObject().put("activity",tree.getActivityClassName()).put("root",JevEvidence.controlSnapshot(tree.getRootNode())));}
758 return id;
759 }
760 private static JSONObject pendingPayload(JSONObject original,Map<String,Candidate> pending)throws JSONException {
761 JSONObject payload=JevPairwiseEvidenceView.shallowCopy(original);JSONArray ids=new JSONArray(),pairs=new JSONArray();Set<String> selected=new HashSet<String>();
762 for(Candidate c:pending.values()){ids.put(c.candidateId);selected.add(c.candidateId);}
763 JSONArray originalPairs=payload.getJSONArray("pairs");
764 for(int i=0;i<originalPairs.length();i++)if(selected.contains(originalPairs.getJSONObject(i).getString("candidate")))pairs.put(originalPairs.get(i));
765 payload.put("candidate_ids",ids).put("pairs",pairs);return payload;
766 }
767 public void poll() {if(synchronous)return;reviewLock.lock();try {
768 for(Map.Entry<String,Cache> item:new ArrayList<Map.Entry<String,Cache>>(pages.entrySet())) {
769 Cache cache=item.getValue();
770 JevTransport.Result response=cache.call.consume();if(response==null)continue;
771 consumeCache(item.getKey(),cache,response);
772 }
773 } finally {reviewLock.unlock();}}
774
775 /** Apply one complete provider receipt atomically, after the response is consumed once. */
776 private boolean consumeCache(String cacheKey,Cache cache,JevTransport.Result response) {
777 JevDeadline.check();
778 if(response==null)return false;
779 Candidate first=cache.candidates.isEmpty()?null:cache.candidates.values().iterator().next();
780 if(cache.sourceVersion!=modelVersion
781 || first==null || cache.historyRevision!=registry.executionRevision(first.action.getState(),first.action)) {
782 receipt("semantic-response-"+cache.request.requestId,responseReceipt(cache,response,null,"STALE_HISTORY_OR_VERSION"));
783 inFlight.removeAll(cache.candidates.keySet());pages.remove(cacheKey);return false;
784 }
785 if(response.error!=null) {
786 requestFailure(cache.request,cache.attempt,response.error,response.httpStatus,response);
787 inFlight.removeAll(cache.candidates.keySet());pages.remove(cacheKey);return false;
788 }
789 try {
790 final String providerRaw=response.raw; final JevHistoryPairwiseProtocol.Request providerRequest=cache.request;
791 JevHistoryPairwiseProtocol.Result result=local(new Callable<JevHistoryPairwiseProtocol.Result>() {
792 public JevHistoryPairwiseProtocol.Result call()throws Exception{return JevHistoryPairwiseProtocol.fromProvider(providerRaw,providerRequest);}
793 });
794 Map<String,JevReviewLedger.PairResult> pairResults=new LinkedHashMap<String,JevReviewLedger.PairResult>();
795 if(!result.pairAnswers.isEmpty())
796 for(Map.Entry<String,JSONObject> pair:result.pairAnswers.entrySet())pairResults.put(pair.getKey(),new JevReviewLedger.PairResult(pair.getValue()));
797 cache.valid=true;JSONArray decisions=new JSONArray();
798 java.util.List<JevReviewLedger.Update> updates=new java.util.ArrayList<JevReviewLedger.Update>();
799 java.util.List<Object[]> auditEvents=new java.util.ArrayList<Object[]>();
800 boolean stale=false;
801 for(JevHistoryPairwiseProtocol.Decision decision:result.decisions.values()) {
802 JevDeadline.check();
803 Candidate candidate=cache.byId.get(decision.candidateId);
804 GUITree currentTree=candidate==null?null:candidate.action.getResolvedGUITree();
805 boolean current=candidate!=null && candidate.action.getState()==cache.sourceState && cache.candidates.containsKey(candidate.key) && currentTree!=null
806 && currentTree.getCurrentState()==cache.sourceState
807 && cache.sourceGui.equals(JevEvidence.guiIdentity(currentTree))
808 && !registry.hasExactExecution(candidate.action.getState(),candidate.action,candidate.action.getResolvedGUITree(),candidate.node);
809 if(!current) { if(candidate!=null){stale=true;inFlight.remove(candidate.key);} continue; }
810 inFlight.remove(candidate.key);String outcome=decision.promote()?"PROBE":"REPRESENTED";
811 String ledgerStatus="REVIEWED";String entryId=entryId(cache.request.requestId,candidate.candidateId,cache.request.inputFingerprint);
812 if(decision.resolved && decision.confidence>0.5) {
813 updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),outcome,decision.confidence,decision.representativeModelActionId,
814 cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId));ledgerStatus="STAGED";
815 auditEvents.add(new Object[]{decision.promote()?"DECISION_PROBE":"DECISION_REPRESENTED",currentStep,
816 candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(),
817 decision.audit(JevRegistry.fields("entry_id",entryId,"request_id",cache.request.requestId,"candidate_id",candidate.candidateId,
818 "confidence",decision.confidence,"choice",decision.representativeModelActionId,
819 "parsed_outcome",outcome,"ledger_status",ledgerStatus,"decision_role","candidate_decision",
820 "input_signature",cache.request.inputSignature,"concrete_identity",JevEvidence.digest(identity(candidate).toString()),
821 "decision_scope",JevEvidence.digest(scope(candidate)),"representative_model_action_id",decision.representativeModelActionId,
822 "decision_request_id",cache.request.requestId))});
823 if(!registry.hasExactExecution(candidate.action.getState(),candidate.action,candidate.action.getResolvedGUITree(),candidate.node))cache.decisions.put(candidate.key,decision);
824 } else {
825 updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),"UNRESOLVED",decision.confidence,decision.representativeModelActionId,
826 cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId));
827 auditEvents.add(new Object[]{"DECISION_REVIEWED",currentStep,candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(),
828 decision.audit(JevRegistry.fields("entry_id",entryId,"request_id",cache.request.requestId,"candidate_id",candidate.candidateId,"confidence",decision.confidence,
829 "choice",decision.representativeModelActionId,"parsed_outcome","UNRESOLVED",
830 "ledger_status","STAGED","decision_role","candidate_decision",
831 "input_signature",cache.request.inputSignature,"concrete_identity",JevEvidence.digest(identity(candidate).toString()),
832 "decision_scope",JevEvidence.digest(scope(candidate)),
833 "representative_model_action_id",decision.representativeModelActionId,"decision_request_id",cache.request.requestId))});
834 }
835 decisions.put(decision.audit(new JSONObject().put("candidate_id",candidate.candidateId)
836 .put("aggregate_choice",decision.representativeModelActionId).put("aggregate_confidence",decision.confidence)
837 .put("parsed_outcome",decision.resolved && decision.confidence>0.5?outcome:"UNRESOLVED").put("ledger_status",ledgerStatus)
838 .put("entry_id",entryId).put("request_id",cache.request.requestId)));
839 }
840 receipt("semantic-response-"+cache.request.requestId,responseReceipt(cache,response,decisions,stale?"STALE_RESULT":null));
841 registry.semanticEvent(stale?"JEV_RESULT_STALE":"SEMANTIC_RESPONSE_CONSUMED",currentStep,null,null,JevRegistry.fields(
842 "request_id",cache.request.requestId,"candidate_count",cache.candidates.size(),"parsed_outcome",stale?"STALE":"STAGED",
843 "wait_ns",response.waitNanos(),"latency_ns",response.latencyNanos()));
844 JevDeadline.check();
845 for(Object[] event:auditEvents)registry.semanticEvent((String)event[0],(Long)event[1],(String)event[2],(String)event[3],(JSONObject)event[4]);
846 if(!registry.reviewLedger().putBeforeDeadline(updates,JevDeadline.absolute()))throw new JevDeadline.Expired();
847 // Publish raw Pair answers only after the whole response passed the
848 // current GUI, State, revision and execution checks above.
849 if(!stale && !pairResults.isEmpty())registry.reviewLedger().putPairResults(pairResults);
850 // STAGED is a candidate judgment. Publication is observed separately from Native BOUND.
851 for(JevReviewLedger.Update update:updates) {
852 JevReviewLedger.Decision d=update.decision,old=update.previous;
853 Candidate candidate=cache.byId.get(d.candidateId);
854 JSONObject fields=JevRegistry.fields("entry_id",entryId(d.requestId,d.candidateId,d.inputFingerprint),
855 "request_id",d.requestId,"candidate_id",d.candidateId,"decision_role","candidate_decision",
856 "ledger_status",d.outcome,"input_signature",cache.request.inputSignature,
857 "input_fingerprint",d.inputFingerprint,"concrete_identity",JevEvidence.digest(identity(candidate).toString()),
858 "decision_scope",JevEvidence.digest(scope(candidate)));
859 if(old!=null && old.requestId!=null && old.candidateId!=null
860 && !entryId(old.requestId,old.candidateId,old.inputFingerprint).equals(entryId(d.requestId,d.candidateId,d.inputFingerprint)))
861 fields.put("replaces_entry_id",entryId(old.requestId,old.candidateId,old.inputFingerprint));
862 registry.semanticEvent("SEMANTIC_DECISION_STORED",currentStep,candidate.action.getState().getGraphId(),candidate.action.getGraphId(),fields);
863 }
864 pages.remove(cacheKey);return !stale;
865 } catch(JSONException error) {
866 receipt("semantic-response-"+cache.request.requestId,responseReceipt(cache,response,null,"PARSE_ERROR:"+error.getMessage()));
867 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",cache.request.requestId,
868 "reason","PARSE_ERROR","error",error.getMessage()==null?"":error.getMessage()));
869 inFlight.removeAll(cache.candidates.keySet());pages.remove(cacheKey);return false;
870 }
871 }
872
873 private void requestFailure(JevHistoryPairwiseProtocol.Request request,int attempt,String error,int status,JevTransport.Result response) {
874 try {
875 JSONObject receipt=response==null?new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response")
876 .put("request_id",request.requestId).put("attempt",attempt).put("retry_count",attempt-1).put("endpoint",endpoint).put("model",model)
877 .put("contract",requestContract(request)).put("step",currentStep).put("input_fingerprint",request.inputFingerprint).put("http_status",status)
878 .put("error",error).put("raw_response",JSONObject.NULL):responseReceipt(new Cache(Collections.<String,Candidate>emptyMap(),request,null,attempt,""),response,null,error);
879 viewMetadata(receipt,request);
880 receipt("semantic-response-"+request.requestId,receipt);
881 } catch(JSONException ignored) { }
882 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",request.requestId,"reason",error,
883 "attempt",attempt,"retry_count",attempt-1,"http_status",status));
884 }
885 public Selection nextSelection(State state,GUITree tree) {if(synchronous){if(!reviewLock.tryLock())return null;}else reviewLock.lock();try (JevRegistry.ConcreteLookup lookup=new JevRegistry.ConcreteLookup()) {
886 if(synchronous && selectionBlocked)return null;
887 poll();if(state==null||tree==null)return null;
888 Page page=preparedPage;
889 if(page==null || state!=preparedState || tree!=preparedTree)return null;
890 for(Candidate candidate:page.candidates.values()) {
891 JevReviewLedger.Decision shared=reusableDecision(candidate,JevEvidence.guiIdentity(tree),state);
892 Cache cached=pages.get(page.cacheKey);
893 JevHistoryPairwiseProtocol.Decision decision=shared==null?null:new JevHistoryPairwiseProtocol.Decision(candidate.candidateId,
894 shared.representativeModelActionId==null?("PROBE".equals(shared.outcome)?JevHistoryPairwiseProtocol.PROMOTE:"REPRESENTED"):shared.representativeModelActionId,
895 shared.confidence);
896 if(decision==null) {
897 if(cached!=null)decision=cached.decisions.get(candidate.key);
898 }
899 if(shared!=null && !executable(candidate,state,tree))continue;
900 if(decision==null||!decision.promote()||decision.confidence<=0.5||served.contains(candidate.key))continue;
901 if(!navigationMask.allow(state,tree,candidate))continue;
902 // A new GUI binding never changes the original decision/source provenance.
903 String requestId=shared!=null?shared.requestId:(cached==null?null:cached.request.requestId);
904 String fingerprint=shared!=null?shared.inputFingerprint:(cached==null?null:cached.request.inputFingerprint);
905 served.add(candidate.key);return new Selection(candidate,entryId(requestId,shared==null?candidate.candidateId:shared.candidateId,fingerprint),requestId,decision.confidence,decision.representativeModelActionId);
906 }
907 return null;
908 } finally {reviewLock.unlock();}}
909 public Candidate next(State state,GUITree tree) {reviewLock.lock();try {
910 Selection selection=nextSelection(state,tree);return selection==null?null:selection.candidate;
911 } finally {reviewLock.unlock();}}
912 public void beginSelection(){if(synchronous){if(!reviewLock.tryLock())return;}else reviewLock.lock();try {served.clear();} finally {reviewLock.unlock();}}
913 public void endSelection(){if(synchronous){if(!reviewLock.tryLock())return;}else reviewLock.lock();try {served.clear();} finally {reviewLock.unlock();}}
914 private static String key(ModelAction action,GUITreeNode node) {
915 return scope(action)+"|"+ConcreteActionIdentity.of(action.getType().name(),node).toString();
916 }
917 private static ConcreteActionIdentity identity(Candidate candidate) {
918 if(candidate.concreteIdentity==null)candidate.concreteIdentity=ConcreteActionIdentity.of(candidate.action.getType().name(),candidate.node);
919 return candidate.concreteIdentity;
920 }
921 private static String scope(Candidate candidate) {
922 return candidate==null||candidate.evidenceScope==null?"":candidate.evidenceScope;
923 }
924 private String projectedContract() { return contract+":"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION; }
925 private String requestContract(JevHistoryPairwiseProtocol.Request request) {
926 return request.evidenceViewVersion.isEmpty()?contract:contract+":"+request.evidenceViewVersion;
927 }
928 private String effectiveContract(Cache cache) { return requestContract(cache.request); }
929 private static String scope(ModelAction action) {
930 return action==null||action.getState()==null?"":action.getState().getActivity()+"|"+action.getState().getGraphId();
931 }
932 private int nextAttempt(String fingerprint) {
933 Integer current=fingerprintAttempts.get(fingerprint);int next=current==null?1:current+1;fingerprintAttempts.put(fingerprint,next);return next;
934 }
935 private static String entryId(String requestId,String candidateId,String fingerprint) {
936 String base=requestId==null||requestId.length()==0?"semantic-cache-"+(fingerprint==null?"none":fingerprint):requestId;
937 return base+"-"+(candidateId==null?"unknown":candidateId);
938 }
939 private <T> T local(final Callable<T> action) throws JSONException {
940 if(!synchronous || JevDeadline.absolute()==Long.MAX_VALUE) {
941 try{return action.call();}catch(JSONException e){throw e;}catch(Exception e){throw new JSONException(e.toString());}
942 }
943 JevDeadline.check();final long deadline=JevDeadline.absolute();Future<T> job;
944 try {job=receiptWorker.submit(new Callable<T>(){public T call()throws Exception {
945 try(JevDeadline scope=JevDeadline.enter(deadline)){JevDeadline.check();return action.call();}
946 }});}catch(RejectedExecutionException e){throw new JevDeadline.Expired();}
947 try{return job.get(Math.max(0,deadline-System.nanoTime()),TimeUnit.NANOSECONDS);}
948 catch(TimeoutException e){job.cancel(false);receiptWorker.purge();throw new JevDeadline.Expired();}
949 catch(InterruptedException e){job.cancel(false);Thread.currentThread().interrupt();throw new JevDeadline.Expired();}
950 catch(ExecutionException e){if(e.getCause() instanceof JevDeadline.Expired)throw new JevDeadline.Expired();throw new JSONException(e.getCause().toString());}
951 }
952 private void receipt(final String name,final JSONObject value) {
953 if(receiptSink==null)return;
954 try {
955 boolean saved=local(new Callable<Boolean>(){public Boolean call(){return receiptSink.save(name,value);}});
956 if(!saved){
957 registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",name,"reason","RECEIPT_WRITE_FAILED"));
958 if(synchronous)throw new JevDeadline.Expired();
959 }
960 }catch(JSONException e){throw new IllegalStateException("receipt worker failed",e);}
961 }
962 private JSONObject requestReceipt(JevHistoryPairwiseProtocol.Request request,Map<String,Candidate> candidates,int attempt) {
963 try {JSONArray ids=new JSONArray();for(Candidate c:candidates.values())ids.put(c.candidateId);
964 JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_request":"async_semantic_request").put("request_id",request.requestId)
965 .put("attempt",attempt).put("endpoint",endpoint).put("model",model).put("contract",requestContract(request)).put("step",currentStep)
966 .put("retry_count",attempt-1)
967 .put("request_lifetime_ms",synchronous?requestLifetimeMs:timeoutMs).put("late_response_reuse",synchronous)
968 .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint).put("candidate_ids",ids)
969 .put("pair_count",request.questionCount).put("history_coverage",request.plan.getJSONObject("coverage"))
970 .put("history_provenance",request.plan.getJSONObject("history_provenance"))
971 .put("request_bytes",request.requestBytes).put("timeout_ms",timeoutMs).put("body",new JSONObject(request.bodyJson));
972 if(evidenceAudit!=null)value.put("local_audit",evidenceAudit.review(auditApe,request,candidates,currentStep,modelVersion));
973 viewMetadata(value,request);return value;
974 } catch(JSONException error) { return new JSONObject(); }
975 }
976 private JSONObject responseReceipt(Cache cache,JevTransport.Result response,JSONArray decisions,String error) {
977 try {JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response")
978 .put("request_id",cache.request.requestId).put("attempt",cache.attempt).put("endpoint",endpoint).put("model",model)
979 .put("retry_count",Math.max(0,cache.attempt-1))
980 .put("contract",effectiveContract(cache)).put("step",currentStep).put("input_signature",cache.request.inputSignature)
981 .put("input_fingerprint",cache.request.inputFingerprint).put("http_status",response.httpStatus)
982 .put("error",error==null?(response.error==null?JSONObject.NULL:response.error):error)
983 .put("wait_ns",response.waitNanos()).put("latency_ns",response.latencyNanos())
984 .put("raw_response",response.raw==null?JSONObject.NULL:response.raw);
985 value.put("source_gui_identity",cache.sourceGui).put("source_abstraction_version",cache.sourceVersion)
986 .put("pair_cache_hit",cache.pairCacheHit)
987 .put("response_consumed_ns",System.nanoTime()).put("response_consumed_wall_time_ms",System.currentTimeMillis())
988 .put("source_audit_path","jev-audit/reviews/"+cache.request.requestId+".json");
989 if(decisions!=null)value.put("decisions",decisions);viewMetadata(value,cache.request);return value;
990 } catch(JSONException ignored) { return new JSONObject(); }
991 }
992 private static void viewMetadata(JSONObject receipt,JevHistoryPairwiseProtocol.Request request)throws JSONException {
993 if(!request.evidenceViewVersion.isEmpty())receipt.put("evidence_view_version",request.evidenceViewVersion)
994 .put("source_input_signature",request.sourceInputSignature);
995 }
996 private static String scopeForPage(Map<String,Candidate> candidates) {
997 for(Candidate candidate:candidates.values())return scope(candidate);
998 return "";
999 }
1000 static JSONObject node(GUITreeNode node,String type)throws JSONException {
1001 JevDeadline.check();
1002 Rect b=node.getBoundsInScreen();
1003 String text=node.getText();
1004 JevDeadline.check();
1005 return JevRegistry.fields("action_type",type,"index",node.getIndex(),"class",JevEvidence.boundedText(node.getClassName()),
1006 "resource_id",JevEvidence.boundedText(node.getResourceID()),"text",JevEvidence.boundedText(text),
1007 "content_desc",JevEvidence.boundedText(node.getContentDesc()),"enabled",node.isEnabled(),"clickable",node.isClickable(),
1008 "long_clickable",node.isLongClickable(),"checkable",node.isCheckable(),"checked",node.isChecked(),
1009 "selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected(),"focusable",node.isFocusable(),
1010 "scrollable",node.isScrollable(),"password",node.isPassword(),"input_text",JevEvidence.boundedText(node.getInputText()),
1011 "bounds",new JSONArray().put(b.left).put(b.top).put(b.right).put(b.bottom),"role_signature",JevEvidence.roleSignature(node));
1012 }
1013 private void contextInvalidated(Page page,String reason) {
1014 registry.semanticEvent("SEMANTIC_CONTEXT_INVALIDATED",currentStep,null,null,JevRegistry.fields(
1015 "input_signature",page.payload.optString("input_signature"),"reason",reason,"selection_context_only",true));
1016 }
1017 public void close(){reviewLock.lock();try {
1018 if(closed)return;
1019 closed=true;
1020 receiptWorker.shutdownNow();transport.close();
1021 if(preparedPage!=null)contextInvalidated(preparedPage,"run_closed");
1022 List<JSONObject> snapshot=registry.reviewLedger().snapshot(contract);
1023 List<JSONObject> projected=registry.reviewLedger().snapshot(projectedContract());
1024 snapshot.addAll(registry.reviewLedger().snapshot(contract+":"+JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION));
1025 for(JSONObject row:projected)try {row.put("evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION);}
1026 catch(JSONException error){throw new IllegalStateException(error);}
1027 snapshot.addAll(projected);
1028 for(JSONObject row:snapshot)registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT",currentStep,null,null,row);
1029 registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT_END",currentStep,null,null,JevRegistry.fields("decision_count",snapshot.size()));
1030 pages.clear();retained.clear();inFlight.clear();served.clear();
1031 } finally {reviewLock.unlock();}}
1032}