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}