1package com.android.commands.monkey.ape.jev;23import java.nio.charset.StandardCharsets;4import java.util.*;5import org.json.*;67/** Frozen history-pairwise-offline-v1-20261031; only code aggregates into Probe eligibility. */8final class JevHistoryPairwiseProtocol {9 static final java.nio.charset.Charset UTF8=StandardCharsets.UTF_8;10 static final String VERSION="history-pairwise-offline-v1-20261031";11 static final String POLICY="exploratory-top3-v1";12 /** P1 view marker; the frozen SYSTEM prompt text remains unchanged. */13 static final String EVIDENCE_VIEW_VERSION="candidate-closure-v1";14 static final String PROJECTED_SCHEMA=VERSION+":"+EVIDENCE_VIEW_VERSION;15 static final String INCREMENTAL_VIEW_VERSION="incremental-candidate-closure-v2";16 static final int MAX_HISTORY=3;17 static final String RULES="Judge whether two concrete GUI actions represent the same application-function operation, using only the supplied GUI evidence and page context.\nSAME_OPERATION: the same application function, differing only in parameter, data object, or entry point.\nDIFFERENT_OPERATION: different application functions.\nINSUFFICIENT_EVIDENCE: the visible evidence is insufficient to judge reliably.\nThe same handler, class, resource ID, menu container, or switch is not sufficient evidence of operation equivalence. Judge application functions, not implementation form.";18 /** Exact suffix and insertion used by the archived StartLux P1 comparison. */19 static final String P1_DECISION_STANDARD="\n\nDecision standard:\n"20 +"SAME_OPERATION means the candidate and the executed history are the same GUI operation pattern already represented; differences limited to data instances, entered values, or equivalent entry points are SAME.\n"21 +"DIFFERENT_OPERATION means the candidate is a distinct command, feature entry, or interaction branch that the executed history does not represent and is reasonably worth one additional concrete-action attempt. Do not require proof of new method coverage, a new state, or successful completion.\n"22 +"INSUFFICIENT_EVIDENCE means the supplied GUI and execution history do not support a reliable relation.\n"23 +"Treat a visible menu-command label as evidence of the command. Do not equate a menu command with ordinary text entered into an EditText. Different Checklist rows with the same edit behavior are normally SAME. For a long-pressed EditText, do not infer a different block operation from the block name alone; it may be ordinary text selection. Similar class, resource ID, outer container, or ModelAction does not establish equivalence. Compare operation behavior, not wording alone.";24 static final String PROMOTE="PROMOTE",SAME="SAME_OPERATION",DIFFERENT="DIFFERENT_OPERATION",INSUFFICIENT="INSUFFICIENT_EVIDENCE";25 static final class Request {26 final String requestId,bodyJson,inputSignature,inputFingerprint,sourceInputSignature,evidenceViewVersion;27 final List<String> candidateIds;final int requestBytes,questionCount;28 final JSONObject plan;29 Request(String id,Prepared prepared,JSONObject payload)throws JSONException {30 requestId=id;bodyJson=prepared.bodyJson;plan=new JSONObject(payload.toString());31 inputSignature=prepared.inputSignature;inputFingerprint=prepared.inputFingerprint;requestBytes=prepared.requestBytes;32 sourceInputSignature=payload.optString("source_input_signature",inputSignature);33 evidenceViewVersion=payload.optString("evidence_view_version","");34 questionCount=prepared.questionCount;candidateIds=new ArrayList<String>(prepared.candidateIds);35 }36 }37 static final class Decision {38 final String candidateId,representativeModelActionId;39 final double confidence;final boolean resolved;40 JSONObject audit=new JSONObject();41 Decision(String id,String choice,double score){this(id,choice,score,!"UNRESOLVED".equals(choice));}42 Decision(String id,String choice,double score,boolean certain){candidateId=id;representativeModelActionId=choice;confidence=score;resolved=certain;}43 boolean promote(){return resolved && PROMOTE.equals(representativeModelActionId);}44 JSONObject audit(JSONObject target)throws JSONException {45 for(Iterator<?> it=audit.keys();it.hasNext();){String key=(String)it.next();target.put(key,audit.get(key));}46 return target;47 }48 }49 static final class Result {50 final Map<String,Decision> decisions;51 final Map<String,JSONObject> pairAnswers;52 Result(Map<String,Decision> out,Map<String,JSONObject> pairs){decisions=out;pairAnswers=pairs;}53 }54 static final class BudgetExceeded extends JSONException {55 final int requestBytes,stateBytes,questionsBytes,candidateCount,pairCount,limitBytes;56 BudgetExceeded(int requestBytes,int candidateCount,int pairCount,int stateBytes,int questionsBytes,int limit)throws JSONException {57 super("SEMANTIC_REQUEST_BUDGET_EXCEEDED");58 this.requestBytes=requestBytes;this.candidateCount=candidateCount;this.pairCount=pairCount;this.limitBytes=limit;59 this.stateBytes=stateBytes;this.questionsBytes=questionsBytes;60 }61 }62 static final class Prepared {63 final JSONObject body;64 final String bodyJson,inputSignature,inputFingerprint;65 final int requestBytes,questionCount;66 final List<String> candidateIds;67 Prepared(JSONObject body,JSONObject payload,String bodyJson,String fingerprint,int bytes,int count)throws JSONException {68 this.body=body;this.bodyJson=bodyJson;inputSignature=payload.getString("input_signature");inputFingerprint=fingerprint;69 requestBytes=bytes;questionCount=count;70 candidateIds=new ArrayList<String>();JSONArray ids=payload.getJSONArray("candidate_ids");71 for(int i=0;i<ids.length();i++)candidateIds.add(ids.getString(i));72 }73 Request request(String id,JSONObject payload)throws JSONException{return new Request(id,this,payload);}74 }75 static Request request(String id,String model,JSONObject payload,int maxBytes)throws JSONException {76 Prepared prepared=prepare(model,payload,maxBytes);77 return prepared.request(id,payload);78 }79 static Prepared prepare(String model,JSONObject payload,int maxBytes)throws JSONException {80 String view=payload.optString("evidence_view_version","");81 if(!view.isEmpty() && (!(EVIDENCE_VIEW_VERSION.equals(view)||INCREMENTAL_VIEW_VERSION.equals(view))82 || !(VERSION+":"+view).equals(payload.getJSONObject("state").optString("evidence_schema"))83 || payload.optString("source_input_signature").isEmpty()))throw new JSONException("UNKNOWN_EVIDENCE_VIEW");84 if(view.isEmpty() && (PROJECTED_SCHEMA.equals(payload.getJSONObject("state").optString("evidence_schema"))85 || (VERSION+":"+INCREMENTAL_VIEW_VERSION).equals(payload.getJSONObject("state").optString("evidence_schema"))))86 throw new JSONException("MISSING_EVIDENCE_VIEW");87 long questionStart=JevReviewProfile.start();88 JSONObject questions=new JSONObject(),criteria=new JSONObject()89 .put(SAME,"Same application-function operation; only parameter, data object, or entry point differs.")90 .put(DIFFERENT,"Different application-function operations.")91 .put(INSUFFICIENT,"The current visible GUI evidence is insufficient to determine the operation relation reliably.");92 JSONArray pairs=payload.getJSONArray("pairs");93 Set<String> bindings=new HashSet<String>();94 for(int i=0;i<pairs.length();i++) {95 JevDeadline.check();JSONObject pair=pairs.getJSONObject(i);String c=pair.getString("candidate"),h=pair.getString("history");96 String q=pair.getString("id");97 if(questions.has(q) || !bindings.add(c+"|"+h)98 || !payload.getJSONObject("state").getJSONObject("candidates").has(c)99 || !payload.getJSONObject("state").getJSONObject("executed_actions").has(h))100 throw new JSONException("invalid or duplicate pair binding");101 questions.put(q,new JSONObject().put("type","choice").put("criteria",criteria)102 .put("instructions",RULES+"\n\nIs candidate at `candidates."+c+"` the same application-function operation as executed action at `executed_actions."+h+"`? Interpret each action using its own source_page_id in pages."+P1_DECISION_STANDARD));103 }104 JevReviewProfile.end("questions",questionStart);105 if(questions.length()==0)throw new JSONException("NO_RECOVERABLE_PAIRS");106 JSONObject body=new JSONObject().put("model",model).put("state",payload.getJSONObject("state")).put("questions",questions);107 long serializeStart=JevReviewProfile.start();108 String bodyJson=body.toString();109 JevReviewProfile.end("wire_serialize",serializeStart);110 long sizeStart=JevReviewProfile.start();111 int bytes=bodyJson.getBytes(UTF8).length;112 JevReviewProfile.end("wire_size",sizeStart);113 JevReviewProfile.add("serialized_wire_bytes",0,bytes);114 int limit=Math.min(maxBytes,32768);115 if(bytes>limit)throw new BudgetExceeded(bytes,payload.getJSONArray("candidate_ids").length(),questions.length(),116 body.getJSONObject("state").toString().getBytes(UTF8).length,questions.toString().getBytes(UTF8).length,limit);117 return new Prepared(body,payload,bodyJson,JevEvidence.digest(bodyJson),bytes,questions.length());118 }119 static Result fromProvider(String raw,Request request)throws JSONException {120 JSONObject answers=new JSONObject(raw).getJSONObject("answers"),questions=new JSONObject(request.bodyJson).getJSONObject("questions");121 if(answers.length()!=questions.length())throw new JSONException("incomplete pair answers");122 for(Iterator<?> it=answers.keys();it.hasNext();)if(!questions.has((String)it.next()))throw new JSONException("unknown pair answer");123 JSONArray pairs=request.plan.getJSONArray("pairs");124 Map<String,List<JSONObject>> byCandidate=new LinkedHashMap<String,List<JSONObject>>();125 Map<String,JSONObject> pairAnswers=new LinkedHashMap<String,JSONObject>();126 for(int i=0;i<pairs.length();i++) {127 JevDeadline.check();JSONObject pair=pairs.getJSONObject(i),answer=answers.getJSONObject(pair.getString("id"));128 String choice=answer.getString("choice");129 if(!SAME.equals(choice)&&!DIFFERENT.equals(choice)&&!INSUFFICIENT.equals(choice))throw new JSONException("invalid pair choice");130 double confidence=answer.getDouble("confidence");131 if(!Double.isFinite(confidence)||confidence<0||confidence>1)throw new JSONException("invalid pair confidence");132 if(answer.has("probabilities")) {133 JSONObject probabilities=answer.getJSONObject("probabilities");134 if(probabilities.length()!=3)throw new JSONException("invalid pair probabilities");135 for(String option:new String[]{SAME,DIFFERENT,INSUFFICIENT}) {136 double value=probabilities.getDouble(option);137 if(!Double.isFinite(value)||value<0||value>1)throw new JSONException("invalid pair probability");138 }139 }140 String c=pair.getString("candidate");List<JSONObject> values=byCandidate.get(c);141 if(values==null){values=new ArrayList<JSONObject>();byCandidate.put(c,values);}values.add(answer);142 pairAnswers.put(pairFingerprint(request,pair),new JSONObject(answer.toString()));143 }144 Map<String,Decision> decisions=new LinkedHashMap<String,Decision>();145 JSONObject coverage=request.plan.getJSONObject("coverage");146 for(String c:request.candidateIds) {147 JSONObject expected=coverage.getJSONObject(c);List<JSONObject> values=byCandidate.get(c);148 double same=0;int different=0,rawSame=0,rawDifferent=0,insufficient=0,low=0,trustedSame=0;149 if(values!=null)for(JSONObject value:values) {150 double confidence=value.getDouble("confidence");String choice=value.getString("choice");151 if(SAME.equals(choice))rawSame++;152 if(DIFFERENT.equals(choice))rawDifferent++;153 if(INSUFFICIENT.equals(choice))insufficient++;154 if(confidence<=0.5)low++;155 if(confidence>0.5 && SAME.equals(choice)){same=Math.max(same,confidence);trustedSame++;}156 if(confidence>0.5 && DIFFERENT.equals(choice))different++;157 }158 int count=expected.getInt("selected_history"),comparisons=values==null?0:values.size();159 if(count<0 || count>MAX_HISTORY || comparisons!=count)throw new JSONException("invalid selected history coverage");160 boolean full=expected.getBoolean("complete") && count==expected.getInt("legal_history_count");161 Decision decision;162 if(same>0)decision=new Decision(c,"REPRESENTED",same);163 else if(different>0)decision=new Decision(c,PROMOTE,same==0?maxTrustedDifferent(values):0);164 else decision=new Decision(c,"UNRESOLVED",comparisons==1?values.get(0).getDouble("confidence"):0,false);165 decision.audit=new JSONObject().put("aggregation_policy",POLICY)166 .put("total_history",expected.getInt("legal_history_count")).put("history_complete",expected.optBoolean("history_collection_complete",expected.getBoolean("complete")))167 .put("unknown_mapping_count",expected.optInt("unknown_mapping_count"))168 .put("recoverable_history_count",expected.getInt("recoverable_history_count"))169 .put("selected_history",count).put("comparison_count",comparisons)170 .put("same_count",rawSame).put("different_count",rawDifferent).put("insufficient_count",insufficient)171 .put("trusted_same_count",trustedSame).put("trusted_different_count",different).put("low_confidence_count",low)172 .put("full_history_compared",full).put("exploratory",decision.promote() && !full);173 decisions.put(c,decision);174 }175 return new Result(decisions,pairAnswers);176 }177 private static double maxTrustedDifferent(List<JSONObject> values)throws JSONException {178 double max=0;for(JSONObject value:values)if(DIFFERENT.equals(value.getString("choice")) && value.getDouble("confidence")>0.5)max=Math.max(max,value.getDouble("confidence"));return max;179 }180 /** Stable identity for one Pair; excludes unrelated candidates in the same request. */181 static String pairFingerprint(Request request,JSONObject pair)throws JSONException {182 JSONObject wire=new JSONObject(request.bodyJson);183 JSONObject state=wire.getJSONObject("state"),candidate=state.getJSONObject("candidates").getJSONObject(pair.getString("candidate"));184 JSONObject history=state.getJSONObject("executed_actions").getJSONObject(pair.getString("history"));185 JSONArray pages=new JSONArray();186 for(JSONObject action:new JSONObject[]{candidate,history}) {187 String page=action.optString("source_page_id","");188 if(!page.isEmpty() && state.getJSONObject("pages").has(page))pages.put(state.getJSONObject("pages").get(page));189 }190 JSONObject candidateKey=new JSONObject(candidate.toString());candidateKey.remove("source_page_id");191 JSONObject historyKey=new JSONObject(history.toString());historyKey.remove("source_page_id");192 JSONObject value=new JSONObject().put("protocol",VERSION).put("model",wire.getString("model"))193 .put("contract",request.plan.optString("review_contract","")).put("provider_endpoint",request.plan.optString("provider_endpoint","")).put("state_id",request.plan.optString("state_id","")).put("model_version",request.plan.optLong("model_version",-1)).put("evidence_schema",state.optString("evidence_schema","")).put("pair_role","candidate-vs-history")194 .put("criteria",new JSONObject().put(SAME,"Same application-function operation; only parameter, data object, or entry point differs.").put(DIFFERENT,"Different application-function operations.").put(INSUFFICIENT,"The current visible GUI evidence is insufficient to determine the operation relation reliably."))195 .put("rules",RULES+P1_DECISION_STANDARD).put("candidate",candidateKey).put("history",historyKey)196 .put("history_provenance",request.plan.getJSONObject("history_provenance").optJSONObject(pair.getString("history")))197 .put("pages",pages);198 return JevEvidence.digest(JevSharedEvidence.canonical(value));199 }200 /** Attribute ranking only; equal signatures are NOT asserted to be semantic equivalents. */201 static List<String> selectHistory(JSONObject candidate,Map<String,JSONObject> history)throws JSONException {202 final Map<String,Set<String>> features=new LinkedHashMap<String,Set<String>>();203 for(String id:history.keySet()){JevDeadline.check();features.put(id,selectionFeatures(history.get(id)));}204 return selectHistory(candidate,history.keySet(),features);205 }206 static List<String> selectHistory(JSONObject candidate,Collection<String> historyIds,Map<String,Set<String>> features)throws JSONException {207 return selectHistoryFeatures(selectionFeatures(candidate),historyIds,features);208 }209 static List<String> selectHistoryFeatures(Set<String> wanted,Collection<String> historyIds,Map<String,Set<String>> features)throws JSONException {210 final Map<String,Integer> scores=new HashMap<String,Integer>();211 List<String> ranked=new ArrayList<String>(historyIds);212 for(String id:ranked){JevDeadline.check();Set<String> f=features.get(id);213 if(f==null)throw new JSONException("missing cached history selection features");214 int score=0;for(String value:f)if(wanted.contains(value))score+=value.startsWith("label:")?100:value.startsWith("word:")?10:value.startsWith("resource_id:")?8:2;215 scores.put(id,score);}216 // Stable sort preserves the Registry's chronological / identity tie order.217 Collections.sort(ranked,new Comparator<String>(){public int compare(String a,String b){return Integer.compare(scores.get(b),scores.get(a));}});218 List<String> selected=new ArrayList<String>();Set<Set<String>> seen=new HashSet<Set<String>>();219 while(!ranked.isEmpty() && selected.size()<MAX_HISTORY){220 JevDeadline.check();String next=ranked.get(0);221 if(!selected.isEmpty())for(String id:ranked)if(!seen.contains(features.get(id))){next=id;break;}222 selected.add(next);seen.add(features.get(next));ranked.remove(next);223 }224 return selected;225 }226 static Set<String> selectionFeatures(JSONObject control)throws JSONException {227 Set<String> out=new TreeSet<String>();if(control==null)return out;228 addFeatures(control,out);JSONArray subtree=control.optJSONArray("target_subtree");229 if(subtree!=null)for(int i=0;i<subtree.length();i++)addFeatures(subtree.getJSONObject(i),out);230 return out;231 }232 private static void addFeatures(JSONObject node,Set<String> out)throws JSONException {233 for(String field:new String[]{"text","content_desc","resource_id","class","action_type"}) {234 addSelectionFeature(field,node.optString(field,""),out);235 }236 }237 static void addSelectionFeature(String field,String raw,Set<String> out) {238 String value=raw.trim().toLowerCase(Locale.ROOT);if(value.isEmpty())return;239 if(field.equals("text")||field.equals("content_desc")){240 out.add("label:"+value);for(String word:value.split("[^\\p{L}\\p{N}]+"))if(!word.isEmpty())out.add("word:"+word);241 }else out.add(field+":"+value);242 }243 private JevHistoryPairwiseProtocol(){}244}