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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.AtomicLong;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.graphics.Rect;
import com.android.commands.monkey.ape.model.ModelAction;
import com.android.commands.monkey.ape.model.State;
import com.android.commands.monkey.ape.tree.GUITree;
import com.android.commands.monkey.ape.tree.GUITreeNode;
import com.android.commands.monkey.ape.utils.Config;

/** Candidate versus same-group confirmed history; Native scheduling remains outside this class. */
public final class JevSemanticReview implements AutoCloseable {
    public static final int MIN_CONCRETE_ACTIONS=3;
    private static final int MAX_CANDIDATES_PER_ROUND=3;
    private static final int MAX_RETRIES=1;
    public interface ReceiptSink { boolean save(String name,JSONObject receipt); }
    public static final class Candidate {
        public final ModelAction action;
        public final GUITreeNode node;
        public final String key;
        public final String candidateId;
        String evidenceScope;
        String reviewContract;
        ConcreteActionIdentity concreteIdentity;
        JevReviewLedger.Staged staged;
        GUITree auditSource;
        final Map<String,com.android.commands.monkey.ape.tree.GUITreeAction> auditHistory=new LinkedHashMap<String,com.android.commands.monkey.ape.tree.GUITreeAction>();
        Candidate(ModelAction a,GUITreeNode n,String k,String id){action=a;node=n;key=k;candidateId=id;}
    }
    public static final class Selection {
        public final Candidate candidate;
        public final String entryId,requestId,candidateId;
        public final String concreteIdentity,decisionScope,inputSignature;
        public final double confidence;
        public final String representativeModelActionId;
        Selection(Candidate c,String entry,String request,double score,String representative) {
            candidate=c;entryId=entry;requestId=request;candidateId=c.staged==null?c.candidateId:c.staged.decision.candidateId;confidence=score;representativeModelActionId=representative;
            concreteIdentity=JevEvidence.digest(identity(c).toString());decisionScope=JevEvidence.digest(scope(c));
            inputSignature=c.evidenceScope==null?null:c.evidenceScope.substring(c.evidenceScope.lastIndexOf('|')+1);
        }
    }
    private static final class Cache {
        final Map<String,Candidate> candidates;
        final Map<String,Candidate> byId;
        final JevHistoryPairwiseProtocol.Request request;
        final JevTransport.Call call;
        final int attempt;
        final String sourceGui;
        final State sourceState;
        final Map<String,JevHistoryPairwiseProtocol.Decision> decisions=new HashMap<String,JevHistoryPairwiseProtocol.Decision>();
        boolean valid;
        boolean pairCacheHit;
        long sourceVersion,historyRevision;int confirmationCount;
        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();
            byId=new HashMap<String,Candidate>();for(Candidate candidate:c.values())byId.put(candidate.candidateId,candidate);}
    }
    private final JevTransport transport;
    private final JevRegistry registry;
    private final JevSemanticNavigationMask navigationMask;
    private final String model;
    private final String endpoint;
    private final int timeoutMs,maxBytes,requestLifetimeMs,retainedCapacity;
    private final AtomicLong sequence=new AtomicLong();
    private final Map<String,Cache> pages=new HashMap<String,Cache>();
    // Completed raw responses stay on their Call. Only the APE thread may validate/apply them.
    private final Map<String,Cache> retained=new LinkedHashMap<String,Cache>();
    private long modelVersion=-1;
    private final Set<String> inFlight=new HashSet<String>();
    private final Set<String> served=new HashSet<String>();
    /* Run-local cursor for candidates that remain eligible after a bounded
       review.  The ledger remains the only durable decision store. */
    private final Map<String,Integer> incrementalOffsets=new HashMap<String,Integer>();
    private final String contract;
    private final Map<String,Integer> fingerprintAttempts=new HashMap<String,Integer>();
    private final ReceiptSink receiptSink;
    private JevEvidenceAudit evidenceAudit;
    private com.android.commands.monkey.MonkeySourceApe auditApe;
    void setEvidenceAudit(JevEvidenceAudit audit,com.android.commands.monkey.MonkeySourceApe ape){evidenceAudit=audit;auditApe=ape;}
    private final boolean synchronous;
    private volatile boolean healthy=true;
    private volatile boolean selectionBlocked,localFailure;
    private JevRuntime.Validator nativeValidator;
    private final ReentrantLock reviewLock=new ReentrantLock();
    // Only copied receipts / provider text enter this worker. Never GUI or Registry mutations.
    private final ThreadPoolExecutor receiptWorker=new ThreadPoolExecutor(1,1,0,TimeUnit.MILLISECONDS,
        new ArrayBlockingQueue<Runnable>(4),new ThreadFactory(){public Thread newThread(Runnable r){
            Thread t=new Thread(r,"ape-semantic-local");t.setDaemon(true);return t;}});
    private volatile long currentStep;
    private String reviewId;
    private long reviewEntryNanos;
    private boolean closed;
    private long localEvidenceNanos,providerWaitNanos;
    private Page preparedPage;private State preparedState;private GUITree preparedTree;

    public JevSemanticReview(String endpoint,String token,int queueCapacity,int timeoutMs,int maxBytes,String model,JevRegistry history)throws IOException {
        this(endpoint,token,queueCapacity,timeoutMs,maxBytes,model,history,null,true);
    }
    public JevSemanticReview(String endpoint,String token,int queueCapacity,int timeoutMs,int maxBytes,String model,JevRegistry history,ReceiptSink sink)throws IOException {
        this(endpoint,token,queueCapacity,timeoutMs,maxBytes,model,history,sink,true);
    }
    public JevSemanticReview(String endpoint,String token,int queueCapacity,int timeoutMs,int maxBytes,String model,JevRegistry history,ReceiptSink sink,boolean waitForResponse)throws IOException {
        transport=new JevTransport(endpoint,token,queueCapacity,"probe");
        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;
        requestLifetimeMs=Math.max(timeoutMs,Config.getInteger("ape.jev.timeoutMs",60000));
        retainedCapacity=queueCapacity+1;
    }
    public boolean isSynchronous(){return synchronous;}
    java.util.List<JevReviewLedger.PendingProbe> pendingNavigationProbes(State state) {
        return registry.reviewLedger().pendingProbes(contract,state);
    }
    public boolean isHealthy(){return healthy;}
    public boolean hasLocalFailure(){return localFailure;}
    public void setStep(long step){currentStep=step;}
    void setReviewId(String id,long entryNanos){reviewId=id;reviewEntryNanos=entryNanos;localEvidenceNanos=0;providerWaitNanos=0;}
    long localEvidenceNanos(){return localEvidenceNanos;}
    long providerWaitNanos(){return providerWaitNanos;}
    public void setModelVersion(long version) {reviewLock.lock();try {
        long previous=modelVersion;
        modelVersion=version;
        if(previous!=version && preparedPage!=null)contextInvalidated(preparedPage,"model_version_changed");
        // Existing terminal ledger semantics stay unchanged. Unapplied retained
        // responses require this exact version again before they can be consumed.
    } finally {reviewLock.unlock();}}
    public void setSnapshotIdentity(Map<String,String> identity) {reviewLock.lock();try { transport.setSnapshotIdentity(identity); } finally {reviewLock.unlock();}}
    public void markExecuted(State state,ModelAction action,GUITreeNode node) {reviewLock.lock();try {
        if(state==null||action==null||node==null)return;
        String key=key(action,node);
        ConcreteActionIdentity executedIdentity=ConcreteActionIdentity.of(action.getType().name(),node);
        registry.reviewLedger().markExecutedMatching(contract,state,executedIdentity);
        // Keep immutable request bindings intact; late replies recheck confirmed execution.
        for(Cache cache:pages.values())cache.decisions.remove(key);
        inFlight.remove(key);
        served.remove(key);
    } finally {reviewLock.unlock();}}
    /** Review the current page before APE is allowed to select an action. */
    public void prepare(State state,GUITree tree) { prepare(state,tree,currentStep); }
    public void prepare(State state,GUITree tree,long step) {prepare(state,tree,step,null);}
    void prepare(State state,GUITree tree,long step,JevRuntime.Validator validator) {
        long prepareStart=JevReviewProfile.start();
        // This flag describes this invocation's construction error, not a prior round.
        localFailure=false;localEvidenceNanos=0;providerWaitNanos=0;
        final long absolute=System.nanoTime()+TimeUnit.MILLISECONDS.toNanos(timeoutMs);
        try (JevDeadline budget=JevDeadline.enter(absolute)) {
            long lockStart=JevReviewProfile.start();
            try {
                if(!reviewLock.tryLock(Math.max(0,absolute-System.nanoTime()),TimeUnit.NANOSECONDS)) {
                    healthy=false;selectionBlocked=true;return;
                }
            } catch(InterruptedException e) {Thread.currentThread().interrupt();healthy=false;selectionBlocked=true;return;}
            finally {JevReviewProfile.end("review_lock_wait",lockStart);}
            try {
                currentStep=step;nativeValidator=validator;
                if(reviewId!=null)registry.semanticEvent("SEMANTIC_REVIEW_STARTED",step,state==null?null:state.getGraphId(),null,
                    JevRegistry.fields("review_id",reviewId,"review_entry_ns",reviewEntryNanos));
                try(JevRegistry.ConcreteLookup lookup=new JevRegistry.ConcreteLookup()) {prepareLocked(state,tree,absolute);}
            }
            catch(JevDeadline.Expired e) {
                for(Cache cache:pages.values())retain(cache);
                pages.clear();inFlight.clear();
                healthy=false;selectionBlocked=true;
                try {failOpen(state,"DEADLINE");}catch(JevDeadline.Expired ignored) { /* audit lock busy: do not wait again */ }
            } finally {nativeValidator=null;reviewLock.unlock();}
        } finally {JevReviewProfile.end("prepare_total",prepareStart);}
    }
    private void prepareLocked(State state,GUITree tree,long deadline) {
        selectionBlocked=false;localFailure=false;
        Page previous=preparedPage;preparedPage=null;
        if(previous!=null)contextInvalidated(previous,"prepare_reentered");
        if(state==null||tree==null||tree.getCurrentState()!=state){JevReviewProfile.add("return_invalid_context",0,1);return;}
        JevDeadline.check();
        if(!synchronous)poll();
        // A fresh round requires a >=3-member action. Smaller groups can only
        // supply already accepted pending work. Check that necessary condition
        // before rendering identities or walking exact-execution history.
        long eligibilityStart=JevReviewProfile.start();
        boolean workPossible=hasReviewWork(state,tree);
        JevReviewProfile.end("review_eligibility",eligibilityStart);
        if(!workPossible){JevReviewProfile.add("return_no_review_work",0,1);return;}
        long scanStart=JevReviewProfile.start();
        Map<String,Candidate> lightweight=collectCandidates(state,tree);
        JevReviewProfile.end("candidate_enumeration",scanStart);
        if(lightweight.isEmpty()){JevReviewProfile.add("return_no_candidates",0,1);return;}
        long selectStart=JevReviewProfile.start();
        Set<String> roundKeys=selectRoundKeys(state,tree,lightweight);
        JevReviewProfile.end("round_selection",selectStart);
        if(roundKeys.isEmpty()){JevReviewProfile.add("return_no_round",0,1);return;}
        Map<String,Candidate> reusable=new LinkedHashMap<String,Candidate>();
        for(Candidate candidate:lightweight.values())if(roundKeys.contains(candidate.key)&&candidate.staged!=null
                && "PROBE".equals(candidate.staged.decision.outcome))reusable.put(candidate.key,candidate);
        if(!reusable.isEmpty()) {
            preparedPage=new Page(state.getGraphId()+"|staged",new JSONObject(),reusable);
            preparedState=state;preparedTree=tree;
            JevReviewProfile.add("return_staged_probe",0,1);
            return;
        }
        Page page;long evidenceStart=System.nanoTime();
        long pairStart=JevReviewProfile.start();
        try {page=roundPage(state,tree,roundKeys,lightweight);}finally{localEvidenceNanos+=System.nanoTime()-evidenceStart;JevReviewProfile.end("round_page",pairStart);}
        preparedPage=page;preparedState=state;preparedTree=tree;
        if(page==null||page.candidates.isEmpty()||page.payload.optJSONArray("pairs").length()==0)return;
        // A staged PROMOTE is an executable piece of work.  Do not spend this
        // review window constructing/submitting a new model request when the
        // current GUI can still execute one of the old probes.
        int retries=synchronous?resumeRetained(state,tree,page,deadline):0;
        if(retries<0)return;
        Map<String,Candidate> pending=new LinkedHashMap<String,Candidate>();
        String currentGui=JevEvidence.guiIdentity(tree);
        for(Map.Entry<String,Candidate> entry:page.candidates.entrySet()) {
            Candidate candidate=entry.getValue();
            JevReviewLedger.Staged prior=staged(candidate,currentGui,state);
            if(prior==null && !inFlight.contains(entry.getKey()) && page.payload.optJSONObject("coverage").optJSONObject(candidate.candidateId).optInt("selected_history")>0)pending.put(entry.getKey(),candidate);
        }
        if(pending.isEmpty())return;
        try {
            Page requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,pending),pending);
            if(requestPage.payload.getJSONArray("pairs").length()==0)return;
            Cache cache=pages.get(requestPage.cacheKey);
            if(cache!=null)return;
            JevHistoryPairwiseProtocol.Prepared prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes);
            JevHistoryPairwiseProtocol.Request fullRequest=prepared.request("semantic-cache-"+sequence.incrementAndGet(),requestPage.payload);
            Map<String,JevReviewLedger.PairResult> pairHits=new LinkedHashMap<String,JevReviewLedger.PairResult>();
            for(int i=0;i<requestPage.payload.getJSONArray("pairs").length();i++) {
                JSONObject pair=requestPage.payload.getJSONArray("pairs").getJSONObject(i);
                String key=JevHistoryPairwiseProtocol.pairFingerprint(fullRequest,pair);
                Map<String,JevReviewLedger.PairResult> hit=registry.reviewLedger().pairResults(Collections.singleton(key));
                if(hit.isEmpty()){pairHits.clear();break;}
                pairHits.put(key,hit.get(key));
            }
            if(!pairHits.isEmpty() && pairHits.size()==requestPage.payload.getJSONArray("pairs").length()) {
                JSONObject answers=new JSONObject();JSONArray pairs=requestPage.payload.getJSONArray("pairs");
                for(int i=0;i<pairs.length();i++) {
                    JSONObject pair=pairs.getJSONObject(i);String key=JevHistoryPairwiseProtocol.pairFingerprint(fullRequest,pair);
                    answers.put(pair.getString("id"),pairHits.get(key).json());
                }
                Cache cached=new Cache(requestPage.candidates,fullRequest,null,0,JevEvidence.guiIdentity(tree));
                Candidate firstCandidate=requestPage.candidates.values().iterator().next();
                cached.sourceVersion=modelVersion;cached.historyRevision=registry.executionRevision(state,firstCandidate.action);cached.confirmationCount=registry.confirmationCount();
                pages.put(requestPage.cacheKey,cached);inFlight.addAll(requestPage.candidates.keySet());cached.pairCacheHit=true;
                for(Candidate candidate:requestPage.candidates.values())bindSource(candidate,cached.sourceGui,cached.historyRevision);
                consumeCache(requestPage.cacheKey,cached,JevTransport.Result.cached(new JSONObject().put("answers",answers).toString()));
                return;
            }
            Map<ConcreteActionIdentity,JevReviewLedger.Decision> fingerprintHits=
                registry.reviewLedger().byFingerprint(requestPage.payload.optString("evidence_view_version").isEmpty()?contract:contract+":"+requestPage.payload.optString("evidence_view_version"),scopeForPage(pending),prepared.inputFingerprint);
            Map<String,Candidate> fingerprintMisses=new LinkedHashMap<String,Candidate>();
            for(Map.Entry<String,Candidate> entry:pending.entrySet())
                if(!fingerprintHits.containsKey(identity(entry.getValue())))fingerprintMisses.put(entry.getKey(),entry.getValue());
            if(fingerprintMisses.isEmpty())return;
            if(fingerprintMisses.size()!=pending.size()) {
                requestPage=new Page(page.cacheKey+"|pending",pendingPayload(page.payload,fingerprintMisses),fingerprintMisses);
                if(requestPage.payload.getJSONArray("pairs").length()==0)return;
                prepared=JevHistoryPairwiseProtocol.prepare(model,requestPage.payload,maxBytes);
            }
            JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),requestPage.payload);
            while(true) {
                long remaining=deadline-System.nanoTime();
                if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) {
                    failOpen(state,remaining<=0?"DEADLINE":"INTERRUPTED");return;
                }
                JevDeadline.check();
                int attempt=nextAttempt(request.inputFingerprint);
                // Capture before submitting, so expiry cannot orphan an accepted call.
                String sourceGui=JevEvidence.guiIdentity(tree);
                long historyRevision=registry.executionRevision(state,requestPage.candidates.values().iterator().next().action);
                receipt("semantic-request-"+request.requestId,requestReceipt(request,requestPage.candidates,attempt));
                registry.semanticEvent("JEV_REQUEST_SENT",currentStep,state.getGraphId(),null,JevRegistry.fields(
                    "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",
                    "attempt",attempt,"input_fingerprint",request.inputFingerprint));
                JevDeadline.check();
                JevTransport.Call call=synchronous
                    ?transport.submitRetained(request.requestId,request.bodyJson,requestLifetimeMs,Math.max(16384,maxBytes))
                    :transport.submit(request.requestId,request.bodyJson,timeoutMs,Math.max(16384,maxBytes));
                if(call==null) {
                    requestFailure(request,attempt,"QUEUE_FULL",0,null);
                    if(retries++<MAX_RETRIES){
                        request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes);continue;
                    }
                    failOpen(state,"QUEUE_FULL");return;
                }
                Cache submitted=new Cache(requestPage.candidates,request,call,attempt,sourceGui);
                submitted.sourceVersion=modelVersion;submitted.historyRevision=historyRevision;
                submitted.confirmationCount=registry.confirmationCount();
                pages.put(requestPage.cacheKey,submitted);
                if(synchronous)archiveOnArrival(submitted,currentStep);
                inFlight.addAll(requestPage.candidates.keySet());
                for(Candidate candidate:pending.values())bindSource(candidate,sourceGui,historyRevision);
                if(!synchronous)return;
                registry.semanticEvent("SEMANTIC_WAIT_STARTED",currentStep,state.getGraphId(),null,JevRegistry.fields(
                    "request_id",request.requestId,"candidate_count",requestPage.candidates.size(),"attempt",attempt));
                JevTransport.Result response=await(call,deadline);
                JevDeadline.check();
                if(response==null)throw new JevDeadline.Expired();
                boolean completed=consumeCache(requestPage.cacheKey,submitted,response);
                if(completed)return;
                if(retries++>=MAX_RETRIES){failOpen(state,"RETRIES_EXHAUSTED");return;}
                request=JevHistoryPairwiseProtocol.request("semantic-"+sequence.incrementAndGet(),model,requestPage.payload,maxBytes);
            }
        } catch(JSONException ignored) {
            if("SEMANTIC_REQUEST_BUDGET_EXCEEDED".equals(ignored.getMessage())) {
                if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) {
                    try {
                        if(submitProjected(state,tree,page,pending,deadline,retries))return;
                    } catch(JSONException projectedError) {
                        healthy=false;localFailure=true;selectionBlocked=true;
                        registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields(
                            "reason","REQUEST_BUILD_FAILED","error",projectedError.getMessage(),
                            "evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION));
                        return;
                    }
                }
                selectionBlocked=true;
                JSONObject fields=JevRegistry.fields("reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId);
                if(ignored instanceof JevHistoryPairwiseProtocol.BudgetExceeded) {
                    JevHistoryPairwiseProtocol.BudgetExceeded size=(JevHistoryPairwiseProtocol.BudgetExceeded)ignored;
                    JevRegistry.merge(fields,JevRegistry.fields("request_bytes",size.requestBytes,"state_bytes",size.stateBytes,
                        "questions_bytes",size.questionsBytes,"candidate_count",size.candidateCount,"pair_count",size.pairCount,"limit_bytes",size.limitBytes));
                }
                registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,fields);
            } else {
                healthy=false;localFailure=true;selectionBlocked=true;
                registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields("reason","REQUEST_BUILD_FAILED","error",ignored.getMessage()));
            }
        }
    }
    private boolean hasReviewWork(State state,GUITree tree) {
        for(ModelAction action:state.getActions()) {
            JevDeadline.check();
            if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue;
            GUITreeNode[] nodes=action.getResolvedNodes();
            if(nodes!=null&&nodes.length>=MIN_CONCRETE_ACTIONS&&action.getResolvedGUITree()==tree)return true;
        }
        // Do not apply the fresh-round size threshold to persistent Probes.
        return registry.reviewLedger().hasPendingProbe(contract,state);
    }
    private Map<String,Candidate> collectCandidates(State state,GUITree tree) {
        Map<String,Candidate> candidates=new LinkedHashMap<String,Candidate>();Set<String> ambiguous=new HashSet<String>();int number=0;
        for(ModelAction action:state.getActions()) {
            JevDeadline.check();
            if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue;
            GUITreeNode[] nodes=action.getResolvedNodes();
            if(nodes==null||nodes.length==0||action.getResolvedGUITree()!=tree)continue;
            for(GUITreeNode node:nodes) {
                JevDeadline.check();if(registry.hasExactExecution(state,action,tree,node))continue;
                ConcreteActionIdentity concrete=ConcreteActionIdentity.of(action.getType().name(),node);
                String id="c"+(++number),identity=scope(action)+"|"+concrete.toString();
                if(ambiguous.contains(identity))continue;
                if(candidates.containsKey(identity)){candidates.remove(identity);ambiguous.add(identity);}
                else {Candidate c=new Candidate(action,node,identity,id);c.concreteIdentity=concrete;candidates.put(identity,c);}
            }
        }
        return candidates;
    }
    private JevReviewLedger.Staged staged(Candidate c,String gui,State state){
        long start=JevReviewProfile.start();
        try {return registry.reviewLedger().staged(contract,state,c.action,identity(c),gui,modelVersion,registry.executionRevision(state,c.action),registry.confirmationCount());}
        finally {JevReviewProfile.end("staged_lookup",start);}
    }
    private boolean executable(Candidate c,State state,GUITree tree){
        if(c.staged==null||!"PROBE".equals(c.staged.decision.outcome)||!c.node.isEnabled()
                ||!c.action.isEnabled()||!c.action.isValid()||c.action.getState()!=state||tree.getCurrentState()!=state)return false;
        if(JevRegistry.correspondingNode(c.staged.source,tree,c.action)!=c.node || !navigationMask.allow(state,tree,c))return false;
        if(nativeValidator==null)return true;
        ModelAction action=c.action;GUITreeNode previous=action.getResolvedNode();
        com.android.commands.monkey.ape.tree.GUITreeAction previousAction=action.getResolvedGUITreeAction();
        int throttle=action.getThrottle();
        try {action.resolveAt((int)currentStep,throttle,tree,c.node,action.getResolvedNodes());return nativeValidator.valid(action);}
        finally {action.resolveAt((int)currentStep,throttle,tree,previous,action.getResolvedNodes());action.setResolvedGUITreeAction(previousAction);}
    }
    private Set<String> selectRoundKeys(State state,GUITree tree,Map<String,Candidate> all) {
        long guiStart=JevReviewProfile.start();String gui=JevEvidence.guiIdentity(tree);JevReviewProfile.end("round_gui_identity",guiStart);
        List<Candidate> reusable=new ArrayList<Candidate>(),fresh=new ArrayList<Candidate>();
        for(Candidate c:all.values()){
            c.staged=staged(c,gui,state);
            if(c.staged!=null){
                c.evidenceScope=c.staged.scope;c.reviewContract=c.staged.contract;
                if(executable(c,state,tree))reusable.add(c);
            } else if(c.node.isEnabled()&&c.action.getResolvedNodes().length>=MIN_CONCRETE_ACTIONS)fresh.add(c);
        }
        Set<String> out=new LinkedHashSet<String>();
        if(!reusable.isEmpty()){for(Candidate c:reusable)out.add(c.key);return out;}
        if(fresh.isEmpty())return out;
        String cursorKey=state.getGraphId()+"|"+scope(fresh.get(0).action);
        int offset=incrementalOffsets.containsKey(cursorKey)?incrementalOffsets.get(cursorKey):0;
        offset=((offset%fresh.size())+fresh.size())%fresh.size();
        // Small pages retain their established behavior; large pages never
        // materialize a half-page only to discard most of it at byte checking.
        int count=Math.min(MAX_CANDIDATES_PER_ROUND,fresh.size()<=3?fresh.size():(fresh.size()+1)/2);
        for(int i=0;i<count;i++)out.add(fresh.get((offset+i)%fresh.size()).key);
        incrementalOffsets.put(cursorKey,offset+count);return out;
    }
    private JevReviewLedger.Decision reusableDecision(Candidate c,String gui,State state) {
        JevReviewLedger.Staged value=staged(c,gui,state);
        if(value==null||!"PROBE".equals(value.decision.outcome))return null;
        c.staged=value;c.evidenceScope=value.scope;c.reviewContract=value.contract;return value.decision;
    }
    private void bindSource(Candidate c,String gui,long revision){
        registry.reviewLedger().bindSource(c.reviewContract==null?contract:c.reviewContract,scope(c),identity(c),gui,
            modelVersion,revision,registry.confirmationCount(),new com.android.commands.monkey.ape.tree.GUITreeAction(c.action.getResolvedGUITree(),c.node,c.action));
    }
    /**
     * Fit an already bounded round to one complete candidate-closure request.
     * Top-3 and coverage/provenance come from the frozen round plan; this
     * method never ranks a projection or expands the remaining page inventory.
     */
    private boolean submitProjected(State state,GUITree tree,Page page,Map<String,Candidate> pending,long deadline,int retries)throws JSONException {
        JevPairwiseEvidenceView.Plan projection=new JevPairwiseEvidenceView.Plan(page.payload);
        List<Candidate> ordered=new ArrayList<Candidate>();
        for(Candidate candidate:pending.values())
            if(page.payload.getJSONObject("coverage").getJSONObject(candidate.candidateId).getInt("selected_history")>0)ordered.add(candidate);
        int offset=0;
        while(offset<ordered.size()) {
            LinkedHashMap<String,Candidate> batch=new LinkedHashMap<String,Candidate>();
            JevHistoryPairwiseProtocol.Prepared prepared=null;JSONObject projected=null;
            while(offset<ordered.size()) {
                JevDeadline.check();Candidate next=ordered.get(offset);batch.put(next.key,next);
                List<String> selectedIds=new ArrayList<String>();
                for(Candidate c:batch.values())selectedIds.add(c.candidateId);
                long projectStart=JevReviewProfile.start();JSONObject trial=projection.project(selectedIds);
                JevReviewProfile.end("closure_projection",projectStart);
                try {
                    JevHistoryPairwiseProtocol.Prepared candidatePrepared=JevHistoryPairwiseProtocol.prepare(model,trial,maxBytes);
                    prepared=candidatePrepared;projected=trial;offset++;
                } catch(JevHistoryPairwiseProtocol.BudgetExceeded tooLarge) {
                    batch.remove(next.key);
                    if(!batch.isEmpty())break;
                    // An indivisible candidate + its complete original Top-3
                    // cannot fit. Skip that closure; do not discard evidence.
                    budgetFailure(state,tooLarge,next.candidateId,JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION);
                    offset++;
                }
            }
            if(batch.isEmpty())continue;
            if(prepared==null || projected==null)throw new JSONException("PROJECTED_BATCH_EMPTY");
            for(Candidate candidate:batch.values()) {
                candidate.reviewContract=projectedContract();
                candidate.evidenceScope=scope(candidate.action)+"|"+prepared.inputSignature;
            }
            Map<ConcreteActionIdentity,JevReviewLedger.Decision> hits=registry.reviewLedger().byFingerprint(
                projectedContract(),scopeForPage(batch),prepared.inputFingerprint);
            boolean allCached=true;
            for(Candidate candidate:batch.values())if(!hits.containsKey(identity(candidate))){allCached=false;break;}
            if(allCached)continue; // no Request.plan deep copy on a view-local cache hit
            String cacheKey=page.cacheKey+"|"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION+"|"+prepared.inputFingerprint;
            if(pages.containsKey(cacheKey))continue;
            JevHistoryPairwiseProtocol.Request request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);
            while(true) {
                long remaining=deadline-System.nanoTime();
                if(synchronous && (remaining<=0 || Thread.currentThread().isInterrupted())) {failOpen(state,"DEADLINE");return true;}
                JevDeadline.check();int attempt=nextAttempt(request.inputFingerprint);
                String sourceGui=JevEvidence.guiIdentity(tree);long historyRevision=registry.executionRevision(state,batch.values().iterator().next().action);
                receipt("semantic-request-"+request.requestId,requestReceipt(request,batch,attempt));
                registry.semanticEvent("JEV_REQUEST_SENT",currentStep,state.getGraphId(),null,JevRegistry.fields(
                    "request_id",request.requestId,"review_id",reviewId,"candidate_count",batch.size(),"pair_count",request.questionCount,
                    "request_bytes",request.requestBytes,"wait_mode",synchronous?"synchronous":"asynchronous","attempt",attempt,
                    "input_fingerprint",request.inputFingerprint,"evidence_view_version",request.evidenceViewVersion));
                JevDeadline.check();
                JevTransport.Call call=synchronous
                    ?transport.submitRetained(request.requestId,request.bodyJson,requestLifetimeMs,Math.max(16384,maxBytes))
                    :transport.submit(request.requestId,request.bodyJson,timeoutMs,Math.max(16384,maxBytes));
                if(call==null) {
                    requestFailure(request,attempt,"QUEUE_FULL",0,null);
                    if(retries++<MAX_RETRIES){request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);continue;}
                    failOpen(state,"QUEUE_FULL");return true;
                }
                Cache submitted=new Cache(batch,request,call,attempt,sourceGui);
                submitted.sourceVersion=modelVersion;submitted.historyRevision=historyRevision;submitted.confirmationCount=registry.confirmationCount();
                pages.put(cacheKey,submitted);if(synchronous)archiveOnArrival(submitted,currentStep);
                inFlight.addAll(batch.keySet());
                for(Candidate candidate:batch.values())bindSource(candidate,sourceGui,historyRevision);
                if(!synchronous)break;
                registry.semanticEvent("SEMANTIC_WAIT_STARTED",currentStep,state.getGraphId(),null,JevRegistry.fields(
                    "request_id",request.requestId,"candidate_count",batch.size(),"attempt",attempt,
                    "evidence_view_version",request.evidenceViewVersion));
                JevTransport.Result response=await(call,deadline);JevDeadline.check();
                if(response==null)throw new JevDeadline.Expired();
                if(consumeCache(cacheKey,submitted,response))break;
                if(retries++>=MAX_RETRIES){failOpen(state,"RETRIES_EXHAUSTED");return true;}
                request=prepared.request("semantic-"+sequence.incrementAndGet(),projected);
            }
            // One bounded logical request per Probe entry.  Remaining eligible
            // candidates stay unreviewed and are selected by the next round's
            // rotating cursor; this avoids turning a large page into an
            // implicit packet fan-out.
            return true;
        }
        return true;
    }
    private void budgetFailure(State state,JevHistoryPairwiseProtocol.BudgetExceeded size,String candidateId,String version) {
        registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,state.getGraphId(),null,JevRegistry.fields(
            "reason","SEMANTIC_REQUEST_BUDGET_EXCEEDED","parsed_outcome","UNRESOLVED","review_id",reviewId,
            "candidate_id",candidateId,"evidence_view_version",version,"request_bytes",size.requestBytes,
            "state_bytes",size.stateBytes,"questions_bytes",size.questionsBytes,"candidate_count",size.candidateCount,
            "pair_count",size.pairCount,"limit_bytes",size.limitBytes));
    }
    private JevTransport.Result await(JevTransport.Call call,long deadline) {
        long began=System.nanoTime();
        try{return call.awaitUntil(deadline);}finally{providerWaitNanos+=System.nanoTime()-began;}
    }
    private void failOpen(State state,String reason) {
        healthy=false;selectionBlocked=true;
        registry.semanticEvent("SEMANTIC_FAIL_OPEN",currentStep,state.getGraphId(),null,
            JevRegistry.fields("reason",reason,"fallback","native","semantic_selection_blocked",true));
    }
    private void retain(Cache cache) {
        retained.put(cache.request.requestId,cache);
        while(retained.size()>retainedCapacity) {
            Iterator<Map.Entry<String,Cache>> it=retained.entrySet().iterator();
            Cache evicted=it.next().getValue();it.remove();evicted.call.cancel("RETAINED_CAPACITY");
        }
    }
    /** Archive raw bytes even if APE never revisits. No decisions are parsed or applied here. */
    private void archiveOnArrival(Cache cache,final long sourceStep) {
        if(receiptSink==null)return;
        final JevHistoryPairwiseProtocol.Request request=cache.request;
        final long sourceVersion=cache.sourceVersion,historyRevision=cache.historyRevision;
        final String sourceGui=cache.sourceGui;
        cache.call.onComplete(new JevTransport.Completion(){public void completed(final JevTransport.Result result) {
            try {receiptWorker.execute(new Runnable(){public void run() {
                try {
                    JSONObject value=new JSONObject().put("schema",1).put("kind","semantic_provider_arrival")
                        .put("request_id",request.requestId).put("endpoint",endpoint).put("model",model)
                        .put("contract",requestContract(request)).put("source_step",sourceStep).put("source_version",sourceVersion)
                        .put("history_revision",historyRevision).put("source_gui",sourceGui)
                        .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint)
                        .put("http_status",result.httpStatus).put("error",result.error==null?JSONObject.NULL:result.error)
                        .put("raw_response",result.raw==null?JSONObject.NULL:result.raw)
                        .put("submitted_ns",result.submittedNanos).put("sent_ns",result.sentNanos)
                        .put("completed_ns",result.completedNanos).put("wait_ns",result.waitNanos())
                        .put("validated",false).put("applied",false);
                    viewMetadata(value,request);
                    if(!receiptSink.save("semantic-arrival-"+request.requestId,value))healthy=false;
                }catch(JSONException e){healthy=false;}catch(RuntimeException e){healthy=false;}
            }});}catch(RejectedExecutionException e){healthy=false;}
        }});
    }
    /** A later review may reuse raw work only against the same visible input and history. */
    private int resumeRetained(State state,GUITree tree,Page page,long deadline) {
        for(Map.Entry<String,Cache> item:new ArrayList<Map.Entry<String,Cache>>(retained.entrySet())) {
            JevDeadline.check();Cache old=item.getValue();
            if(old.sourceVersion!=modelVersion || old.sourceState!=state) {
                old.call.cancel("STALE_HISTORY_OR_VERSION");retained.remove(item.getKey());continue;
            }
            JevTransport.Result response=old.call.poll();
            String expectedInput=JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION.equals(old.request.evidenceViewVersion)?old.request.inputSignature:old.request.sourceInputSignature;
            if(!expectedInput.equals(page.payload.optString("input_signature"))) {
                if(response!=null && response.error!=null)retained.remove(item.getKey());
                continue;
            }
            if(old.historyRevision!=registry.executionRevision(state,old.candidates.values().iterator().next().action)) {
                old.call.cancel("STALE_HISTORY_OR_VERSION");retained.remove(item.getKey());continue;
            }
            if(!old.sourceGui.equals(JevEvidence.guiIdentity(tree)))continue;
            Map<String,Candidate> current=new LinkedHashMap<String,Candidate>();
            for(String key:old.candidates.keySet()) {
                Candidate c=page.candidates.get(key);
                if(c!=null && old.byId.containsKey(c.candidateId) && old.byId.get(c.candidateId).key.equals(key))current.put(key,c);
            }
            if(current.size()!=old.request.candidateIds.size()) {
                old.call.cancel("STALE_CANDIDATES");retained.remove(item.getKey());continue;
            }
            if(!old.request.evidenceViewVersion.isEmpty())for(Candidate candidate:current.values()) {
                candidate.evidenceScope=scope(candidate.action)+"|"+old.request.inputSignature;
                candidate.reviewContract=effectiveContract(old);
            }
            Cache rebound=new Cache(current,old.request,old.call,old.attempt,JevEvidence.guiIdentity(tree));
            rebound.sourceVersion=old.sourceVersion;rebound.historyRevision=old.historyRevision;
            rebound.confirmationCount=old.confirmationCount;
            // Keep it recoverable even if parsing/receipt processing exhausts this later budget.
            pages.put(item.getKey(),rebound);retained.remove(item.getKey());
            if(response==null)response=await(old.call,deadline);
            JevDeadline.check();
            if(response==null)throw new JevDeadline.Expired();
            if(consumeCache(item.getKey(),rebound,response)) {
                if(old.request.evidenceViewVersion.isEmpty())return -1;
                continue;
            }
            // Protocol / HTTP failures remain failures, rather than reusable semantic answers.
            return 1;
        }
        return 0;
    }
    private static final class Page {
        final String cacheKey;final JSONObject payload;final Map<String,Candidate> candidates;
        Page(String k,JSONObject p,Map<String,Candidate> c){cacheKey=k;payload=p;candidates=c;}
    }
    private Page page(final State state,final GUITree tree) { return page(state,tree,null); }
    private Page page(final State state,final GUITree tree,final Set<String> include) {
        return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<Page>() {
            public Page get(){try(JevRegistry.ConcreteLookup lookup=new JevRegistry.ConcreteLookup()){return buildPage(state,tree,include);}}
        });
    }
    private Page roundPage(final State state,final GUITree tree,final Set<String> include,final Map<String,Candidate> descriptors){
        return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<Page>(){
            public Page get(){return buildPage(state,tree,include,descriptors);}
        });
    }
    private Page buildPage(State state,GUITree tree,Set<String> include){return buildPage(state,tree,include,null);}
    private Page buildPage(State state,GUITree tree,Set<String> include,Map<String,Candidate> descriptors) {
        long began=System.nanoTime();
        try {
            JevRegistry.ConfirmedHistory history=registry.confirmedHistory(state);
            long evidenceStart=System.nanoTime();
            Map<String,Candidate> candidates=new LinkedHashMap<String,Candidate>();Set<String> ambiguous=new HashSet<String>();int number=0;
            if(descriptors!=null){for(Candidate c:descriptors.values())if(include.contains(c.key))candidates.put(c.key,c);}
            else for(ModelAction action:state.getActions()) {
                JevDeadline.check();
                if(!action.requireTarget()||action.isBack()||!action.isValid()||!action.isEnabled())continue;
                GUITreeNode[] nodes=action.getResolvedNodes();
                if(nodes==null||nodes.length<MIN_CONCRETE_ACTIONS||action.getResolvedGUITree()!=tree)continue;
                for(GUITreeNode node:nodes) {
                    JevDeadline.check();if(registry.hasExactExecution(state,action,tree,node))continue;
                    String id="c"+(++number),identity=key(action,node);
                    if(include!=null&&!include.contains(identity))continue;
                    if(ambiguous.contains(identity))continue;
                    if(candidates.containsKey(identity)){candidates.remove(identity);ambiguous.add(identity);}
                    else {Candidate c=new Candidate(action,node,identity,id);c.concreteIdentity=ConcreteActionIdentity.of(action.getType().name(),node);candidates.put(identity,c);}
                }
            }
            if(candidates.isEmpty())return null;
            JSONObject pages=new JSONObject(),current=new JSONObject(),executed=new JSONObject(),coverage=new JSONObject(),provenance=new JSONObject();
            JSONArray pairs=new JSONArray(),ids=new JSONArray();
            JSONObject recoverable=new JSONObject();Set<String> usedHistory=new java.util.LinkedHashSet<String>(),usedPages=new HashSet<String>();
            IdentityHashMap<GUITree,String> pageIds=new IdentityHashMap<GUITree,String>();
            IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,String> historyIds=new IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,String>();
            IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,JevEvidence.PairDescriptor> historyDescriptors=new IdentityHashMap<com.android.commands.monkey.ape.tree.GUITreeAction,JevEvidence.PairDescriptor>();
            Map<String,com.android.commands.monkey.ape.tree.GUITreeAction> historyById=new LinkedHashMap<String,com.android.commands.monkey.ape.tree.GUITreeAction>();
            int recoverableHistoryCount=0;
            for(Candidate c:candidates.values()) {
                JevDeadline.check();ids.put(c.candidateId);c.auditSource=tree;
                String pageId=sourcePage(tree,pages,pageIds);
                long descriptorStartCurrent=JevReviewProfile.start();
                JevEvidence.PairDescriptor candidateDescriptor=descriptors==null?null:JevEvidence.pairDescriptor(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action));
                JevReviewProfile.end("candidate_descriptor",descriptorStartCurrent);
                long richStart=JevReviewProfile.start();
                JSONObject control=descriptors==null?JevEvidence.pairAction(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action),pageId):null;
                if(descriptors==null)JevReviewProfile.end("candidate_rich",richStart);
                if(control!=null)current.put(c.candidateId,control);
                List<com.android.commands.monkey.ape.tree.GUITreeAction> members=history.members(c.action);
                Map<String,Set<String>> featurePool=new LinkedHashMap<String,Set<String>>();
                for(com.android.commands.monkey.ape.tree.GUITreeAction h:members) {
                    JevDeadline.check();String hid=historyIds.get(h);
                    if(hid==null) {
                        hid="h"+(historyIds.size()+1);historyIds.put(h,hid);
                        historyById.put(hid,h);
                        sourcePage(h.getGUITree(),pages,pageIds); // preserve old Page ID allocation order
                        long descriptorStart=JevReviewProfile.start();
                        JevEvidence.PairDescriptor descriptor=JevEvidence.pairDescriptor(h);
                        JevReviewProfile.end("history_descriptor",descriptorStart);
                        historyDescriptors.put(h,descriptor);
                        if(descriptor.recoverable)recoverableHistoryCount++;
                        provenance.put(hid,new JSONObject().put("source_tree",h.getGUITree().getId())
                            .put("source_gui_timestamp",h.getGUITree().getTimestamp()).put("current_state",state.getGraphId())
                            .put("current_model_action",h.getModelAction().getGraphId()).put("current_version",modelVersion)
                            .put("input_confirmed",true).put("confirmation_basis","live confirmedInputs before review; not function completion")
                            .put("attributes_recoverable",descriptor.recoverable));
                    }
                    JevEvidence.PairDescriptor descriptor=historyDescriptors.get(h);
                    if(descriptor!=null && descriptor.recoverable)featurePool.put(hid,descriptor.selectionFeatures);
                }
                long rankStart=JevReviewProfile.start();
                List<String> selected=descriptors==null?(control==null?Collections.<String>emptyList():JevHistoryPairwiseProtocol.selectHistory(control,featurePool.keySet(),featurePool))
                    :(!candidateDescriptor.recoverable?Collections.<String>emptyList():JevHistoryPairwiseProtocol.selectHistoryFeatures(candidateDescriptor.selectionFeatures,featurePool.keySet(),featurePool));
                JevReviewProfile.end("history_ranking",rankStart);
                if(descriptors!=null && !selected.isEmpty()){
                    long currentRich=JevReviewProfile.start();control=JevEvidence.pairAction(new com.android.commands.monkey.ape.tree.GUITreeAction(tree,c.node,c.action),pageId);
                    JevReviewProfile.end("candidate_rich",currentRich);if(control==null)throw new IllegalStateException("selected candidate became unrecoverable");
                    current.put(c.candidateId,control);
                }
                long pairStart=JevReviewProfile.start();JSONArray selectedIds=new JSONArray();
                for(String hid:selected){
                    selectedIds.put(hid);usedHistory.add(hid);c.auditHistory.put(hid,historyById.get(hid));
                    pairs.put(new JSONObject().put("id","p"+(pairs.length()+1)).put("candidate",c.candidateId).put("history",hid));
                }
                JevReviewProfile.end("pair_generation",pairStart);
                coverage.put(c.candidateId,new JSONObject().put("legal_history_count",members.size()).put("complete",history.complete && featurePool.size()==members.size())
                    .put("history_collection_complete",history.complete).put("recoverable_history_count",featurePool.size()).put("selected_history",selected.size()).put("selected_history_ids",selectedIds)
                    .put("unknown_mapping_count",history.unknown)
                    .put("mapping_basis","live confirmedInputs; current State/ModelAction object links and node Name")
                    .put("context_recoverable",descriptors==null?control!=null:candidateDescriptor.recoverable));
            }
            // Only the selected history witnesses receive the expensive rich JSON form.
            // Descriptor extraction above has already frozen the exact ranking inputs.
            for(String hid:usedHistory) {
                com.android.commands.monkey.ape.tree.GUITreeAction h=historyById.get(hid);
                if(h==null)throw new IllegalStateException("missing selected history descriptor: "+hid);
                long witnessStart=JevReviewProfile.start();
                JSONObject witness=JevEvidence.pairAction(h,pageIds.get(h.getGUITree()));
                JevReviewProfile.end("history_rich",witnessStart);
                if(witness==null)throw new IllegalStateException("selected history became unrecoverable: "+hid);
                recoverable.put(hid,witness);
            }
            // Send source context only for current candidates and selected witnesses, once per source page.
            for(Iterator<?> it=current.keys();it.hasNext();)usedPages.add(current.getJSONObject((String)it.next()).getString("source_page_id"));
            for(String hid:usedHistory){JSONObject witness=recoverable.getJSONObject(hid);executed.put(hid,witness);usedPages.add(witness.getString("source_page_id"));}
            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);}
            for(String id:discard)pages.remove(id);
            long stateStart=JevReviewProfile.start();
            JSONObject stateEvidence=new JSONObject().put("evidence_schema",JevHistoryPairwiseProtocol.VERSION)
                .put("pages",pages).put("candidates",current).put("executed_actions",executed);
            JSONObject payload=new JSONObject().put("state",stateEvidence).put("pairs",pairs).put("candidate_ids",ids).put("coverage",coverage)
                .put("history_provenance",provenance).put("aggregation_policy",JevHistoryPairwiseProtocol.POLICY)
                .put("review_contract",contract)
                .put("provider_endpoint",endpoint)
                .put("model_version",modelVersion).put("state_id",state.getGraphId())
                .put("confirmation_count",history.total).put("unknown_mapping_count",history.unknown)
                .put("execution_revision",registry.executionRevision(state));
            if(descriptors!=null){
                JSONArray overview=new JSONArray();
                for(Candidate c:descriptors.values())if(!current.has(c.candidateId))overview.put(new JSONArray().put(c.candidateId).put(c.action.getType().name())
                    .put(JevEvidence.boundedText(c.node.getClassName(),64)).put(JevEvidence.boundedText(c.node.getResourceID(),64))
                    .put(JevEvidence.boundedText(c.node.getText(),64)).put(c.node.isEnabled()));
                incrementalView(payload,overview);
            }
            JevReviewProfile.end("state_projection",stateStart);
            long jsonStart=JevReviewProfile.start();
            String serialized=payload.toString();
            String signature=JevEvidence.digest(serialized);payload.put("input_signature",signature);
            JevReviewProfile.end("plan_serialize_signature",jsonStart);
            JevReviewProfile.add("serialized_plan_bytes",0,serialized.getBytes(JevHistoryPairwiseProtocol.UTF8).length);
            for(Candidate c:candidates.values()){c.evidenceScope=scope(c.action)+"|"+signature;c.reviewContract=descriptors==null?contract:contract+":"+JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION;}
            registry.semanticEvent("HISTORY_PAIRWISE_COST",currentStep,state.getGraphId(),null,JevRegistry.fields(
                "history_enumeration_ns",history.enumerationNanos,"evidence_build_ns",System.nanoTime()-evidenceStart,
                "prepare_local_ns",System.nanoTime()-began,"confirmed_input_objects",history.total,"unknown_mapping_count",history.unknown,
                "history_complete",history.complete,"recoverable_history_count",recoverableHistoryCount,"selected_history",executed.length(),"candidate_count",candidates.size(),"pair_count",pairs.length(),
                "evidence_bytes",stateEvidence.toString().getBytes(JevHistoryPairwiseProtocol.UTF8).length));
            return new Page(state.getGraphId()+"|"+signature,payload,candidates);
        }catch(JSONException error){throw new IllegalStateException("pair evidence construction failed",error);}
    }
    /** Shared wire contract for live descriptors and archived-evidence regression. */
    static void incrementalView(JSONObject payload,JSONArray overview)throws JSONException {
        payload.getJSONObject("state").put("evidence_schema",JevHistoryPairwiseProtocol.VERSION+":"+JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION)
            .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"))
                .put("actions",overview).put("view","lightweight inventory; target subtree/parent/local context only for compared candidates"));
        payload.put("evidence_view_version",JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION);
        payload.put("source_input_signature",JevEvidence.digest(payload.toString()));
    }
    private static String sourcePage(GUITree tree,JSONObject pages,IdentityHashMap<GUITree,String> pageIds)throws JSONException {
        String id=pageIds.get(tree);
        if(id==null){id="page"+(pageIds.size()+1);pageIds.put(tree,id);
            pages.put(id,new JSONObject().put("activity",tree.getActivityClassName()).put("root",JevEvidence.controlSnapshot(tree.getRootNode())));}
        return id;
    }
    private static JSONObject pendingPayload(JSONObject original,Map<String,Candidate> pending)throws JSONException {
        JSONObject payload=JevPairwiseEvidenceView.shallowCopy(original);JSONArray ids=new JSONArray(),pairs=new JSONArray();Set<String> selected=new HashSet<String>();
        for(Candidate c:pending.values()){ids.put(c.candidateId);selected.add(c.candidateId);}
        JSONArray originalPairs=payload.getJSONArray("pairs");
        for(int i=0;i<originalPairs.length();i++)if(selected.contains(originalPairs.getJSONObject(i).getString("candidate")))pairs.put(originalPairs.get(i));
        payload.put("candidate_ids",ids).put("pairs",pairs);return payload;
    }
    public void poll() {if(synchronous)return;reviewLock.lock();try {
        for(Map.Entry<String,Cache> item:new ArrayList<Map.Entry<String,Cache>>(pages.entrySet())) {
            Cache cache=item.getValue();
            JevTransport.Result response=cache.call.consume();if(response==null)continue;
            consumeCache(item.getKey(),cache,response);
        }
    } finally {reviewLock.unlock();}}

    /** Apply one complete provider receipt atomically, after the response is consumed once. */
    private boolean consumeCache(String cacheKey,Cache cache,JevTransport.Result response) {
        JevDeadline.check();
        if(response==null)return false;
        Candidate first=cache.candidates.isEmpty()?null:cache.candidates.values().iterator().next();
        if(cache.sourceVersion!=modelVersion
                || first==null || cache.historyRevision!=registry.executionRevision(first.action.getState(),first.action)) {
            receipt("semantic-response-"+cache.request.requestId,responseReceipt(cache,response,null,"STALE_HISTORY_OR_VERSION"));
            inFlight.removeAll(cache.candidates.keySet());pages.remove(cacheKey);return false;
        }
        if(response.error!=null) {
            requestFailure(cache.request,cache.attempt,response.error,response.httpStatus,response);
            inFlight.removeAll(cache.candidates.keySet());pages.remove(cacheKey);return false;
        }
        try {
            final String providerRaw=response.raw; final JevHistoryPairwiseProtocol.Request providerRequest=cache.request;
            JevHistoryPairwiseProtocol.Result result=local(new Callable<JevHistoryPairwiseProtocol.Result>() {
                public JevHistoryPairwiseProtocol.Result call()throws Exception{return JevHistoryPairwiseProtocol.fromProvider(providerRaw,providerRequest);}
            });
            Map<String,JevReviewLedger.PairResult> pairResults=new LinkedHashMap<String,JevReviewLedger.PairResult>();
            if(!result.pairAnswers.isEmpty())
                for(Map.Entry<String,JSONObject> pair:result.pairAnswers.entrySet())pairResults.put(pair.getKey(),new JevReviewLedger.PairResult(pair.getValue()));
            cache.valid=true;JSONArray decisions=new JSONArray();
            java.util.List<JevReviewLedger.Update> updates=new java.util.ArrayList<JevReviewLedger.Update>();
            java.util.List<Object[]> auditEvents=new java.util.ArrayList<Object[]>();
            boolean stale=false;
            for(JevHistoryPairwiseProtocol.Decision decision:result.decisions.values()) {
                JevDeadline.check();
                Candidate candidate=cache.byId.get(decision.candidateId);
                GUITree currentTree=candidate==null?null:candidate.action.getResolvedGUITree();
                boolean current=candidate!=null && candidate.action.getState()==cache.sourceState && cache.candidates.containsKey(candidate.key) && currentTree!=null
                    && currentTree.getCurrentState()==cache.sourceState
                    && cache.sourceGui.equals(JevEvidence.guiIdentity(currentTree))
                    && !registry.hasExactExecution(candidate.action.getState(),candidate.action,candidate.action.getResolvedGUITree(),candidate.node);
                if(!current) { if(candidate!=null){stale=true;inFlight.remove(candidate.key);} continue; }
                inFlight.remove(candidate.key);String outcome=decision.promote()?"PROBE":"REPRESENTED";
                String ledgerStatus="REVIEWED";String entryId=entryId(cache.request.requestId,candidate.candidateId,cache.request.inputFingerprint);
                if(decision.resolved && decision.confidence>0.5) {
                    updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),outcome,decision.confidence,decision.representativeModelActionId,
                        cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId));ledgerStatus="STAGED";
                    auditEvents.add(new Object[]{decision.promote()?"DECISION_PROBE":"DECISION_REPRESENTED",currentStep,
                        candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(),
                        decision.audit(JevRegistry.fields("entry_id",entryId,"request_id",cache.request.requestId,"candidate_id",candidate.candidateId,
                            "confidence",decision.confidence,"choice",decision.representativeModelActionId,
                            "parsed_outcome",outcome,"ledger_status",ledgerStatus,"decision_role","candidate_decision",
                            "input_signature",cache.request.inputSignature,"concrete_identity",JevEvidence.digest(identity(candidate).toString()),
                            "decision_scope",JevEvidence.digest(scope(candidate)),"representative_model_action_id",decision.representativeModelActionId,
                            "decision_request_id",cache.request.requestId))});
                    if(!registry.hasExactExecution(candidate.action.getState(),candidate.action,candidate.action.getResolvedGUITree(),candidate.node))cache.decisions.put(candidate.key,decision);
                } else {
                    updates.add(new JevReviewLedger.Update(effectiveContract(cache),scope(candidate),identity(candidate),"UNRESOLVED",decision.confidence,decision.representativeModelActionId,
                        cache.request.inputFingerprint,cache.request.requestId,candidate.candidateId));
                    auditEvents.add(new Object[]{"DECISION_REVIEWED",currentStep,candidate.action.getState()==null?null:candidate.action.getState().getGraphId(),candidate.action.getGraphId(),
                        decision.audit(JevRegistry.fields("entry_id",entryId,"request_id",cache.request.requestId,"candidate_id",candidate.candidateId,"confidence",decision.confidence,
                            "choice",decision.representativeModelActionId,"parsed_outcome","UNRESOLVED",
                            "ledger_status","STAGED","decision_role","candidate_decision",
                            "input_signature",cache.request.inputSignature,"concrete_identity",JevEvidence.digest(identity(candidate).toString()),
                            "decision_scope",JevEvidence.digest(scope(candidate)),
                            "representative_model_action_id",decision.representativeModelActionId,"decision_request_id",cache.request.requestId))});
                }
                decisions.put(decision.audit(new JSONObject().put("candidate_id",candidate.candidateId)
                    .put("aggregate_choice",decision.representativeModelActionId).put("aggregate_confidence",decision.confidence)
                    .put("parsed_outcome",decision.resolved && decision.confidence>0.5?outcome:"UNRESOLVED").put("ledger_status",ledgerStatus)
                    .put("entry_id",entryId).put("request_id",cache.request.requestId)));
            }
            receipt("semantic-response-"+cache.request.requestId,responseReceipt(cache,response,decisions,stale?"STALE_RESULT":null));
            registry.semanticEvent(stale?"JEV_RESULT_STALE":"SEMANTIC_RESPONSE_CONSUMED",currentStep,null,null,JevRegistry.fields(
                "request_id",cache.request.requestId,"candidate_count",cache.candidates.size(),"parsed_outcome",stale?"STALE":"STAGED",
                "wait_ns",response.waitNanos(),"latency_ns",response.latencyNanos()));
            JevDeadline.check();
            for(Object[] event:auditEvents)registry.semanticEvent((String)event[0],(Long)event[1],(String)event[2],(String)event[3],(JSONObject)event[4]);
            if(!registry.reviewLedger().putBeforeDeadline(updates,JevDeadline.absolute()))throw new JevDeadline.Expired();
            // Publish raw Pair answers only after the whole response passed the
            // current GUI, State, revision and execution checks above.
            if(!stale && !pairResults.isEmpty())registry.reviewLedger().putPairResults(pairResults);
            // STAGED is a candidate judgment. Publication is observed separately from Native BOUND.
            for(JevReviewLedger.Update update:updates) {
                JevReviewLedger.Decision d=update.decision,old=update.previous;
                Candidate candidate=cache.byId.get(d.candidateId);
                JSONObject fields=JevRegistry.fields("entry_id",entryId(d.requestId,d.candidateId,d.inputFingerprint),
                    "request_id",d.requestId,"candidate_id",d.candidateId,"decision_role","candidate_decision",
                    "ledger_status",d.outcome,"input_signature",cache.request.inputSignature,
                    "input_fingerprint",d.inputFingerprint,"concrete_identity",JevEvidence.digest(identity(candidate).toString()),
                    "decision_scope",JevEvidence.digest(scope(candidate)));
                if(old!=null && old.requestId!=null && old.candidateId!=null
                    && !entryId(old.requestId,old.candidateId,old.inputFingerprint).equals(entryId(d.requestId,d.candidateId,d.inputFingerprint)))
                    fields.put("replaces_entry_id",entryId(old.requestId,old.candidateId,old.inputFingerprint));
                registry.semanticEvent("SEMANTIC_DECISION_STORED",currentStep,candidate.action.getState().getGraphId(),candidate.action.getGraphId(),fields);
            }
            pages.remove(cacheKey);return !stale;
        } catch(JSONException error) {
            receipt("semantic-response-"+cache.request.requestId,responseReceipt(cache,response,null,"PARSE_ERROR:"+error.getMessage()));
            registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",cache.request.requestId,
                "reason","PARSE_ERROR","error",error.getMessage()==null?"":error.getMessage()));
            inFlight.removeAll(cache.candidates.keySet());pages.remove(cacheKey);return false;
        }
    }

    private void requestFailure(JevHistoryPairwiseProtocol.Request request,int attempt,String error,int status,JevTransport.Result response) {
        try {
            JSONObject receipt=response==null?new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response")
                .put("request_id",request.requestId).put("attempt",attempt).put("retry_count",attempt-1).put("endpoint",endpoint).put("model",model)
                .put("contract",requestContract(request)).put("step",currentStep).put("input_fingerprint",request.inputFingerprint).put("http_status",status)
                .put("error",error).put("raw_response",JSONObject.NULL):responseReceipt(new Cache(Collections.<String,Candidate>emptyMap(),request,null,attempt,""),response,null,error);
            viewMetadata(receipt,request);
            receipt("semantic-response-"+request.requestId,receipt);
        } catch(JSONException ignored) { }
        registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",request.requestId,"reason",error,
            "attempt",attempt,"retry_count",attempt-1,"http_status",status));
    }
    public Selection nextSelection(State state,GUITree tree) {if(synchronous){if(!reviewLock.tryLock())return null;}else reviewLock.lock();try (JevRegistry.ConcreteLookup lookup=new JevRegistry.ConcreteLookup()) {
        if(synchronous && selectionBlocked)return null;
        poll();if(state==null||tree==null)return null;
        Page page=preparedPage;
        if(page==null || state!=preparedState || tree!=preparedTree)return null;
        for(Candidate candidate:page.candidates.values()) {
            JevReviewLedger.Decision shared=reusableDecision(candidate,JevEvidence.guiIdentity(tree),state);
            Cache cached=pages.get(page.cacheKey);
            JevHistoryPairwiseProtocol.Decision decision=shared==null?null:new JevHistoryPairwiseProtocol.Decision(candidate.candidateId,
                shared.representativeModelActionId==null?("PROBE".equals(shared.outcome)?JevHistoryPairwiseProtocol.PROMOTE:"REPRESENTED"):shared.representativeModelActionId,
                shared.confidence);
            if(decision==null) {
                if(cached!=null)decision=cached.decisions.get(candidate.key);
            }
            if(shared!=null && !executable(candidate,state,tree))continue;
            if(decision==null||!decision.promote()||decision.confidence<=0.5||served.contains(candidate.key))continue;
            if(!navigationMask.allow(state,tree,candidate))continue;
            // A new GUI binding never changes the original decision/source provenance.
            String requestId=shared!=null?shared.requestId:(cached==null?null:cached.request.requestId);
            String fingerprint=shared!=null?shared.inputFingerprint:(cached==null?null:cached.request.inputFingerprint);
            served.add(candidate.key);return new Selection(candidate,entryId(requestId,shared==null?candidate.candidateId:shared.candidateId,fingerprint),requestId,decision.confidence,decision.representativeModelActionId);
        }
        return null;
    } finally {reviewLock.unlock();}}
    public Candidate next(State state,GUITree tree) {reviewLock.lock();try {
        Selection selection=nextSelection(state,tree);return selection==null?null:selection.candidate;
    } finally {reviewLock.unlock();}}
    public void beginSelection(){if(synchronous){if(!reviewLock.tryLock())return;}else reviewLock.lock();try {served.clear();} finally {reviewLock.unlock();}}
    public void endSelection(){if(synchronous){if(!reviewLock.tryLock())return;}else reviewLock.lock();try {served.clear();} finally {reviewLock.unlock();}}
    private static String key(ModelAction action,GUITreeNode node) {
        return scope(action)+"|"+ConcreteActionIdentity.of(action.getType().name(),node).toString();
    }
    private static ConcreteActionIdentity identity(Candidate candidate) {
        if(candidate.concreteIdentity==null)candidate.concreteIdentity=ConcreteActionIdentity.of(candidate.action.getType().name(),candidate.node);
        return candidate.concreteIdentity;
    }
    private static String scope(Candidate candidate) {
        return candidate==null||candidate.evidenceScope==null?"":candidate.evidenceScope;
    }
    private String projectedContract() { return contract+":"+JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION; }
    private String requestContract(JevHistoryPairwiseProtocol.Request request) {
        return request.evidenceViewVersion.isEmpty()?contract:contract+":"+request.evidenceViewVersion;
    }
    private String effectiveContract(Cache cache) { return requestContract(cache.request); }
    private static String scope(ModelAction action) {
        return action==null||action.getState()==null?"":action.getState().getActivity()+"|"+action.getState().getGraphId();
    }
    private int nextAttempt(String fingerprint) {
        Integer current=fingerprintAttempts.get(fingerprint);int next=current==null?1:current+1;fingerprintAttempts.put(fingerprint,next);return next;
    }
    private static String entryId(String requestId,String candidateId,String fingerprint) {
        String base=requestId==null||requestId.length()==0?"semantic-cache-"+(fingerprint==null?"none":fingerprint):requestId;
        return base+"-"+(candidateId==null?"unknown":candidateId);
    }
    private <T> T local(final Callable<T> action) throws JSONException {
        if(!synchronous || JevDeadline.absolute()==Long.MAX_VALUE) {
            try{return action.call();}catch(JSONException e){throw e;}catch(Exception e){throw new JSONException(e.toString());}
        }
        JevDeadline.check();final long deadline=JevDeadline.absolute();Future<T> job;
        try {job=receiptWorker.submit(new Callable<T>(){public T call()throws Exception {
            try(JevDeadline scope=JevDeadline.enter(deadline)){JevDeadline.check();return action.call();}
        }});}catch(RejectedExecutionException e){throw new JevDeadline.Expired();}
        try{return job.get(Math.max(0,deadline-System.nanoTime()),TimeUnit.NANOSECONDS);}
        catch(TimeoutException e){job.cancel(false);receiptWorker.purge();throw new JevDeadline.Expired();}
        catch(InterruptedException e){job.cancel(false);Thread.currentThread().interrupt();throw new JevDeadline.Expired();}
        catch(ExecutionException e){if(e.getCause() instanceof JevDeadline.Expired)throw new JevDeadline.Expired();throw new JSONException(e.getCause().toString());}
    }
    private void receipt(final String name,final JSONObject value) {
        if(receiptSink==null)return;
        try {
            boolean saved=local(new Callable<Boolean>(){public Boolean call(){return receiptSink.save(name,value);}});
            if(!saved){
                registry.semanticEvent("JEV_REQUEST_FAILED",currentStep,null,null,JevRegistry.fields("request_id",name,"reason","RECEIPT_WRITE_FAILED"));
                if(synchronous)throw new JevDeadline.Expired();
            }
        }catch(JSONException e){throw new IllegalStateException("receipt worker failed",e);}
    }
    private JSONObject requestReceipt(JevHistoryPairwiseProtocol.Request request,Map<String,Candidate> candidates,int attempt) {
        try {JSONArray ids=new JSONArray();for(Candidate c:candidates.values())ids.put(c.candidateId);
            JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_request":"async_semantic_request").put("request_id",request.requestId)
                .put("attempt",attempt).put("endpoint",endpoint).put("model",model).put("contract",requestContract(request)).put("step",currentStep)
                .put("retry_count",attempt-1)
                .put("request_lifetime_ms",synchronous?requestLifetimeMs:timeoutMs).put("late_response_reuse",synchronous)
                .put("input_signature",request.inputSignature).put("input_fingerprint",request.inputFingerprint).put("candidate_ids",ids)
                .put("pair_count",request.questionCount).put("history_coverage",request.plan.getJSONObject("coverage"))
                .put("history_provenance",request.plan.getJSONObject("history_provenance"))
                .put("request_bytes",request.requestBytes).put("timeout_ms",timeoutMs).put("body",new JSONObject(request.bodyJson));
            if(evidenceAudit!=null)value.put("local_audit",evidenceAudit.review(auditApe,request,candidates,currentStep,modelVersion));
            viewMetadata(value,request);return value;
        } catch(JSONException error) { return new JSONObject(); }
    }
    private JSONObject responseReceipt(Cache cache,JevTransport.Result response,JSONArray decisions,String error) {
        try {JSONObject value=new JSONObject().put("schema",1).put("kind",synchronous?"sync_semantic_response":"async_semantic_response")
            .put("request_id",cache.request.requestId).put("attempt",cache.attempt).put("endpoint",endpoint).put("model",model)
            .put("retry_count",Math.max(0,cache.attempt-1))
            .put("contract",effectiveContract(cache)).put("step",currentStep).put("input_signature",cache.request.inputSignature)
            .put("input_fingerprint",cache.request.inputFingerprint).put("http_status",response.httpStatus)
            .put("error",error==null?(response.error==null?JSONObject.NULL:response.error):error)
            .put("wait_ns",response.waitNanos()).put("latency_ns",response.latencyNanos())
            .put("raw_response",response.raw==null?JSONObject.NULL:response.raw);
            value.put("source_gui_identity",cache.sourceGui).put("source_abstraction_version",cache.sourceVersion)
                .put("pair_cache_hit",cache.pairCacheHit)
                .put("response_consumed_ns",System.nanoTime()).put("response_consumed_wall_time_ms",System.currentTimeMillis())
                .put("source_audit_path","jev-audit/reviews/"+cache.request.requestId+".json");
            if(decisions!=null)value.put("decisions",decisions);viewMetadata(value,cache.request);return value;
        } catch(JSONException ignored) { return new JSONObject(); }
    }
    private static void viewMetadata(JSONObject receipt,JevHistoryPairwiseProtocol.Request request)throws JSONException {
        if(!request.evidenceViewVersion.isEmpty())receipt.put("evidence_view_version",request.evidenceViewVersion)
            .put("source_input_signature",request.sourceInputSignature);
    }
    private static String scopeForPage(Map<String,Candidate> candidates) {
        for(Candidate candidate:candidates.values())return scope(candidate);
        return "";
    }
    static JSONObject node(GUITreeNode node,String type)throws JSONException {
        JevDeadline.check();
        Rect b=node.getBoundsInScreen();
        String text=node.getText();
        JevDeadline.check();
        return JevRegistry.fields("action_type",type,"index",node.getIndex(),"class",JevEvidence.boundedText(node.getClassName()),
            "resource_id",JevEvidence.boundedText(node.getResourceID()),"text",JevEvidence.boundedText(text),
            "content_desc",JevEvidence.boundedText(node.getContentDesc()),"enabled",node.isEnabled(),"clickable",node.isClickable(),
            "long_clickable",node.isLongClickable(),"checkable",node.isCheckable(),"checked",node.isChecked(),
            "selected",node.getJevSelected()==null?JSONObject.NULL:node.getJevSelected(),"focusable",node.isFocusable(),
            "scrollable",node.isScrollable(),"password",node.isPassword(),"input_text",JevEvidence.boundedText(node.getInputText()),
            "bounds",new JSONArray().put(b.left).put(b.top).put(b.right).put(b.bottom),"role_signature",JevEvidence.roleSignature(node));
    }
    private void contextInvalidated(Page page,String reason) {
        registry.semanticEvent("SEMANTIC_CONTEXT_INVALIDATED",currentStep,null,null,JevRegistry.fields(
            "input_signature",page.payload.optString("input_signature"),"reason",reason,"selection_context_only",true));
    }
    public void close(){reviewLock.lock();try {
        if(closed)return;
        closed=true;
        receiptWorker.shutdownNow();transport.close();
        if(preparedPage!=null)contextInvalidated(preparedPage,"run_closed");
        List<JSONObject> snapshot=registry.reviewLedger().snapshot(contract);
        List<JSONObject> projected=registry.reviewLedger().snapshot(projectedContract());
        snapshot.addAll(registry.reviewLedger().snapshot(contract+":"+JevHistoryPairwiseProtocol.INCREMENTAL_VIEW_VERSION));
        for(JSONObject row:projected)try {row.put("evidence_view_version",JevHistoryPairwiseProtocol.EVIDENCE_VIEW_VERSION);}
            catch(JSONException error){throw new IllegalStateException(error);}
        snapshot.addAll(projected);
        for(JSONObject row:snapshot)registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT",currentStep,null,null,row);
        registry.semanticEvent("SEMANTIC_LEDGER_SNAPSHOT_END",currentStep,null,null,JevRegistry.fields("decision_count",snapshot.size()));
        pages.clear();retained.clear();inFlight.clear();served.clear();
    } finally {reviewLock.unlock();}}
}
