@validate.qa/runner 1.0.37 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1341 -706
  2. package/dist/cli.mjs +1341 -706
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -241,6 +241,9 @@ var require_ai_provider = __commonJS({
241
241
  exports2.getAvailableModels = getAvailableModels;
242
242
  exports2.getAvailableOrchestratorModels = getAvailableOrchestratorModels;
243
243
  exports2.isAvailableModelId = isAvailableModelId;
244
+ exports2.resolveWhisperProvider = resolveWhisperProvider;
245
+ exports2.isWhisperConfigured = isWhisperConfigured;
246
+ exports2.getWhisperModel = getWhisperModel;
244
247
  exports2.getOpenAIWhisperClient = getOpenAIWhisperClient;
245
248
  exports2.resetAIClient = resetAIClient;
246
249
  exports2.getServerProxiedAIClient = getServerProxiedAIClient;
@@ -570,20 +573,70 @@ var require_ai_provider = __commonJS({
570
573
  function isAvailableModelId(modelId) {
571
574
  return getAvailableModels().some((model) => model.id === modelId);
572
575
  }
576
+ var GROQ_API_BASE_DEFAULT = "https://api.groq.com/openai/v1";
577
+ var GROQ_WHISPER_DEFAULT = "whisper-large-v3-turbo";
578
+ var OPENAI_WHISPER_DEFAULT = "whisper-1";
579
+ function isUsableApiKey(value) {
580
+ if (!value)
581
+ return false;
582
+ const trimmed = value.trim();
583
+ if (!trimmed)
584
+ return false;
585
+ if (/your[-_]?.*(key|api|secret)/i.test(trimmed))
586
+ return false;
587
+ if (trimmed === "sk-..." || trimmed === "gsk_...")
588
+ return false;
589
+ return true;
590
+ }
591
+ function resolveWhisperProvider() {
592
+ const forced = (process.env.STT_PROVIDER ?? process.env.WHISPER_PROVIDER ?? "").trim().toLowerCase();
593
+ const hasGroq = isUsableApiKey(process.env.GROQ_API_KEY);
594
+ const hasOpenAI = isUsableApiKey(process.env.OPENAI_API_KEY);
595
+ if (forced === "groq")
596
+ return hasGroq ? "groq" : null;
597
+ if (forced === "openai")
598
+ return hasOpenAI ? "openai" : null;
599
+ if (hasGroq)
600
+ return "groq";
601
+ if (hasOpenAI)
602
+ return "openai";
603
+ return null;
604
+ }
605
+ function isWhisperConfigured() {
606
+ return resolveWhisperProvider() !== null;
607
+ }
608
+ function getWhisperModel(provider) {
609
+ const p = provider ?? resolveWhisperProvider();
610
+ if (p === "groq") {
611
+ return process.env.GROQ_WHISPER_MODEL?.trim() || process.env.OPENAI_WHISPER_MODEL?.trim() || GROQ_WHISPER_DEFAULT;
612
+ }
613
+ return process.env.OPENAI_WHISPER_MODEL?.trim() || OPENAI_WHISPER_DEFAULT;
614
+ }
573
615
  var _whisperClient = null;
616
+ var _whisperClientProvider = null;
574
617
  function getOpenAIWhisperClient() {
575
- if (_whisperClient)
618
+ const provider = resolveWhisperProvider();
619
+ if (!provider) {
620
+ throw new Error("Speech transcription is not configured (set GROQ_API_KEY or OPENAI_API_KEY)");
621
+ }
622
+ if (_whisperClient && _whisperClientProvider === provider) {
576
623
  return _whisperClient;
577
- const apiKey = process.env.OPENAI_API_KEY;
578
- if (!apiKey)
579
- throw new Error("OPENAI_API_KEY is required for Whisper transcription");
580
- _whisperClient = new openai_1.default({ apiKey });
624
+ }
625
+ if (provider === "groq") {
626
+ const apiKey = process.env.GROQ_API_KEY;
627
+ const baseURL = process.env.GROQ_API_BASE?.trim() || GROQ_API_BASE_DEFAULT;
628
+ _whisperClient = new openai_1.default({ apiKey, baseURL });
629
+ } else {
630
+ _whisperClient = new openai_1.default({ apiKey: process.env.OPENAI_API_KEY });
631
+ }
632
+ _whisperClientProvider = provider;
581
633
  return _whisperClient;
582
634
  }
583
635
  var _serverProxiedClient = null;
584
636
  function resetAIClient() {
585
637
  _clientCache.clear();
586
638
  _whisperClient = null;
639
+ _whisperClientProvider = null;
587
640
  _serverProxiedClient = null;
588
641
  }
589
642
  function buildServerProxiedClient() {
@@ -1682,6 +1735,7 @@ var init_constants = __esm({
1682
1735
  "DATABASE_URL",
1683
1736
  "JWT_SECRET",
1684
1737
  "OPENAI_API_KEY",
1738
+ "GROQ_API_KEY",
1685
1739
  "GEMINI_API_KEY",
1686
1740
  "XAI_API_KEY",
1687
1741
  "GROK_API_KEY",
@@ -3248,8 +3302,10 @@ var init_audit_pricing = __esm({
3248
3302
  "MiniMax-M2.7-highspeed": { input: 0.6, output: 2.4 },
3249
3303
  "MiniMax-M2": { input: 0.3, output: 1.2 },
3250
3304
  "deepseek-v4-flash": { input: 0.14, output: 0.28 },
3251
- // Whisper bills per-minute, not per-token
3305
+ // Whisper bills per-minute, not per-token (OpenAI + Groq)
3252
3306
  "whisper-1": null,
3307
+ "whisper-large-v3": null,
3308
+ "whisper-large-v3-turbo": null,
3253
3309
  // Historical / deprecated (kept so old audit rows still show a cost).
3254
3310
  // Deprecated Grok variants priced at the legacy authoritative Grok rate
3255
3311
  // ($0.20/$0.80) those rows were generated under.
@@ -3860,6 +3916,43 @@ function assertedText(stmt) {
3860
3916
  }
3861
3917
  return out.join(" ").toLowerCase();
3862
3918
  }
3919
+ function clickedAdditiveAffordance(lines) {
3920
+ const nameRe = /name\s*:\s*(?:['"`]([^'"`]+)['"`]|\/([^/]+)\/)/g;
3921
+ for (const ln of lines) {
3922
+ if (!/\.click\s*\(/.test(ln))
3923
+ continue;
3924
+ nameRe.lastIndex = 0;
3925
+ let m;
3926
+ while (m = nameRe.exec(ln)) {
3927
+ const name = (m[1] ?? m[2]).toLowerCase();
3928
+ if (EXCLUDE_AFFORDANCE.test(name))
3929
+ continue;
3930
+ if (ADDITIVE_AFFORDANCE.test(name))
3931
+ return true;
3932
+ }
3933
+ }
3934
+ return false;
3935
+ }
3936
+ function matcherText(stmt) {
3937
+ const out = [];
3938
+ const re = /\.(?:toContainText|toHaveText)\s*\(\s*(?:['"`]([^'"`]+)['"`]|\/([^/]+)\/)/g;
3939
+ let m;
3940
+ while (m = re.exec(stmt))
3941
+ out.push(m[1] ?? m[2]);
3942
+ return out.join(" ").toLowerCase();
3943
+ }
3944
+ function isEmptyStateProof(stmt) {
3945
+ if (NEGATED_ASSERT.test(stmt))
3946
+ return false;
3947
+ if (/\.toHaveCount\s*\(\s*0\s*\)/.test(stmt))
3948
+ return true;
3949
+ if (/\.toBe\s*\(\s*0\s*\)/.test(stmt) && !isChromeTarget(stmt))
3950
+ return true;
3951
+ if (VISIBILITY_MATCHER.test(stmt) || TEXT_CONTENT_MATCHER.test(stmt)) {
3952
+ return EMPTY_STATE_TEXT.test(`${assertedText(stmt)} ${matcherText(stmt)}`);
3953
+ }
3954
+ return false;
3955
+ }
3863
3956
  function hasAttributeTransition(stmts) {
3864
3957
  const byAttr = /* @__PURE__ */ new Map();
3865
3958
  for (const { stmt } of stmts) {
@@ -3966,6 +4059,21 @@ function validateAssertionStrength(code, opts = {}) {
3966
4059
  const interacted = mutated || navClicks > 0;
3967
4060
  navOnly = !mutated && navClicks > 0;
3968
4061
  const reloaded = RELOAD.test(body);
4062
+ if (opts.flagContradictedAdditions && mutated && clickedAdditiveAffordance(interactionStmts)) {
4063
+ const empties = stmts.filter(({ stmt }) => isEmptyStateProof(stmt));
4064
+ if (empties.length > 0) {
4065
+ const others = stmts.filter(({ stmt }) => !isEmptyStateProof(stmt));
4066
+ const provesSomethingElse = detectOutcomeProof(others, mutated, false, clickedControlNames(interactionStmts), typedValues(interactionStmts));
4067
+ if (!provesSomethingElse) {
4068
+ return {
4069
+ ok: false,
4070
+ severity: "block",
4071
+ reason: "additive action (add/create/save/favourite) proves no positive outcome \u2014 its only asserted result is that the target collection is still empty; hold @needs-review, do not ship as a passing regression",
4072
+ signals: base2({ interacted, mutated, terminal })
4073
+ };
4074
+ }
4075
+ }
4076
+ }
3969
4077
  const hasOutcomeProof = detectOutcomeProof(stmts, mutated, reloaded, clickedControlNames(interactionStmts), typedValues(interactionStmts));
3970
4078
  const signals = base2({ interacted, mutated, navOnly, hasOutcomeProof, terminal });
3971
4079
  if (hasOutcomeProof) {
@@ -4020,7 +4128,7 @@ function validateAssertionStrength(code, opts = {}) {
4020
4128
  signals
4021
4129
  };
4022
4130
  }
4023
- var ERROR_BANNER, OUTCOME_ALWAYS, FORM_INPUT, ANY_CLICK, NAV_LOCATOR, URL_PROOF, RELOAD, EXPECT_STMT, BLOCK_SCAFFOLD, LOCATOR_BINDING, STATE_MATCHER, CHROME_LOCATOR, CONTENT_ROLE, GET_BY_TEXT, HAS_TEXT_FILTER, OUTCOME_TEXT, MESSAGE_SURFACE, TEXT_CONTENT_MATCHER, VISIBILITY_MATCHER, AUTH_START, AUTH_END, LOGIN_CALL_LINE, NEGATED_ASSERT, TYPED_INPUT, TITLE_LINE, ROLE_NAME_RE, INTERACT_VERB, ROLE_SIG_RE, TRANSIENT_CONTROL_ROLES;
4131
+ var ERROR_BANNER, OUTCOME_ALWAYS, FORM_INPUT, ANY_CLICK, NAV_LOCATOR, URL_PROOF, RELOAD, EXPECT_STMT, BLOCK_SCAFFOLD, LOCATOR_BINDING, STATE_MATCHER, CHROME_LOCATOR, CONTENT_ROLE, GET_BY_TEXT, HAS_TEXT_FILTER, OUTCOME_TEXT, MESSAGE_SURFACE, TEXT_CONTENT_MATCHER, VISIBILITY_MATCHER, ADDITIVE_AFFORDANCE, EXCLUDE_AFFORDANCE, EMPTY_STATE_TEXT, AUTH_START, AUTH_END, LOGIN_CALL_LINE, NEGATED_ASSERT, TYPED_INPUT, TITLE_LINE, ROLE_NAME_RE, INTERACT_VERB, ROLE_SIG_RE, TRANSIENT_CONTROL_ROLES;
4024
4132
  var init_assertion_strength = __esm({
4025
4133
  "../shared/dist/assertion-strength.js"() {
4026
4134
  "use strict";
@@ -4064,6 +4172,9 @@ var init_assertion_strength = __esm({
4064
4172
  MESSAGE_SURFACE = /getByText\s*\(|\.filter\s*\(\s*\{\s*hasText\s*:|getByRole\s*\(\s*['"`](?:alert|alertdialog|status|heading|dialog)['"`]/i;
4065
4173
  TEXT_CONTENT_MATCHER = /\.(toHaveText|toContainText)\s*\(/;
4066
4174
  VISIBILITY_MATCHER = /\.toBeVisible\s*\(/;
4175
+ ADDITIVE_AFFORDANCE = /\b(add|adds|added|create|creates|save|saves|favou?rite|favou?rites|invite|invites|upload|uploads|insert|append|bookmark|follow|subscribe|post|posts|new)\b/i;
4176
+ EXCLUDE_AFFORDANCE = /\b(filter|filters|search|searches|sort|view|views|tab|tabs|window|windows|toggle|expand|collapse|column|columns|zoom|layout)\b/i;
4177
+ EMPTY_STATE_TEXT = /\bno\b[\s\S]{0,24}?\b(yet|found|results?|items?|favou?rites?|entries|records?|matches|posts?|comments?)\b|nothing (?:here|yet|to show|found)|\bempty\b|\(\s*0\s*\)|\b0\s+(?:favou?rites?|results?|items?|entries|records?|matches|posts?|comments?)\b/i;
4067
4178
  AUTH_START = /^\s*\/\/\s*>>>\s*VALIDATE_QA_AUTH/;
4068
4179
  AUTH_END = /^\s*\/\/\s*<<<\s*VALIDATE_QA_AUTH/;
4069
4180
  LOGIN_CALL_LINE = /^\s*await\s+__vqaLogin\s*\(\s*page\s*\)\s*;?\s*$/;
@@ -4086,6 +4197,24 @@ var init_assertion_strength = __esm({
4086
4197
  });
4087
4198
 
4088
4199
  // ../shared/dist/run-classification.js
4200
+ function pickLatestHealthRun(runs) {
4201
+ for (const run2 of runs) {
4202
+ if (run2.mode === "auth-setup")
4203
+ continue;
4204
+ if (run2.status === "PASSED" || run2.status === "FAILED" || run2.status === "ERROR") {
4205
+ return run2;
4206
+ }
4207
+ }
4208
+ return null;
4209
+ }
4210
+ function pickDisplayRun(runs) {
4211
+ if (runs.length === 0)
4212
+ return null;
4213
+ const latest = runs[0];
4214
+ if (latest.status === "PENDING" || latest.status === "RUNNING")
4215
+ return latest;
4216
+ return pickLatestHealthRun(runs) ?? latest;
4217
+ }
4089
4218
  function isEnvironmentalError(run2) {
4090
4219
  if (!run2)
4091
4220
  return false;
@@ -4547,6 +4676,262 @@ var init_origin_domain = __esm({
4547
4676
  }
4548
4677
  });
4549
4678
 
4679
+ // ../shared/dist/voice-config.js
4680
+ function normalizeTtsVoiceId(voiceId) {
4681
+ if (!voiceId)
4682
+ return null;
4683
+ const trimmed = voiceId.trim();
4684
+ if (!trimmed)
4685
+ return null;
4686
+ return LEGACY_TTS_VOICE_ID_MAP[trimmed] ?? trimmed;
4687
+ }
4688
+ function sttModelsForProvider(provider) {
4689
+ if (provider === "openai") {
4690
+ return STT_MODEL_OPTIONS.filter((m) => m.provider === "openai");
4691
+ }
4692
+ if (provider === "groq") {
4693
+ return STT_MODEL_OPTIONS.filter((m) => m.provider === "groq");
4694
+ }
4695
+ return STT_MODEL_OPTIONS;
4696
+ }
4697
+ function isValidSttProvider(value) {
4698
+ return value === "groq" || value === "openai";
4699
+ }
4700
+ function isSttModelForProvider(modelId, provider) {
4701
+ if (!modelId)
4702
+ return false;
4703
+ return STT_MODEL_OPTIONS.some((m) => m.id === modelId && m.provider === provider);
4704
+ }
4705
+ function resolveSttModelForProvider(provider, configuredModel) {
4706
+ if (configuredModel && isSttModelForProvider(configuredModel, provider)) {
4707
+ return configuredModel;
4708
+ }
4709
+ return DEFAULT_STT_MODEL_BY_PROVIDER[provider];
4710
+ }
4711
+ function sanitizeSttErrorMessage(raw) {
4712
+ if (!raw)
4713
+ return "Speech transcription failed.";
4714
+ let msg = String(raw).replace(/\b(sk-proj-|sk-|gsk_|sk-svcacct-)[A-Za-z0-9_-]{8,}/gi, "$1***").replace(/Incorrect API key provided:\s*\S+/gi, "Incorrect API key provided").replace(/You can find your API key at https?:\/\/\S+/gi, "").replace(/\s+/g, " ").trim();
4715
+ if (/401|incorrect api key|invalid.?api.?key|authentication|unauthorized/i.test(msg)) {
4716
+ return "Speech transcription failed: the speech API key was rejected. Prefer GROQ_API_KEY (same as Copilot voice) or a valid OPENAI_API_KEY on the org server.";
4717
+ }
4718
+ if (/429|rate.?limit|too many requests/i.test(msg)) {
4719
+ return "Speech transcription failed: the speech provider rate-limited the request. Try again in a moment.";
4720
+ }
4721
+ if (/not configured|GROQ_API_KEY or OPENAI_API_KEY/i.test(msg)) {
4722
+ return "Speech transcription is not configured. Set GROQ_API_KEY (preferred) or OPENAI_API_KEY on the org server.";
4723
+ }
4724
+ if (msg.length > 280)
4725
+ msg = `${msg.slice(0, 277)}\u2026`;
4726
+ return msg || "Speech transcription failed.";
4727
+ }
4728
+ function isValidTtsVoiceId(value) {
4729
+ const normalized = normalizeTtsVoiceId(value);
4730
+ if (!normalized)
4731
+ return false;
4732
+ return TTS_VOICE_OPTIONS.some((v) => v.id === normalized);
4733
+ }
4734
+ function isValidTtsModel(value) {
4735
+ if (!value)
4736
+ return false;
4737
+ return TTS_MODEL_OPTIONS.some((m) => m.id === value);
4738
+ }
4739
+ var VOICE_PREVIEW_SAMPLE, DEFAULT_TTS_MODEL, DEFAULT_TTS_VOICE_ID, DEFAULT_TTS_SPEED, LEGACY_TTS_VOICE_ID_MAP, DEFAULT_STT_MODEL_BY_PROVIDER, STT_PROVIDER_OPTIONS, STT_MODEL_OPTIONS, TTS_MODEL_OPTIONS, TTS_VOICE_OPTIONS;
4740
+ var init_voice_config = __esm({
4741
+ "../shared/dist/voice-config.js"() {
4742
+ "use strict";
4743
+ VOICE_PREVIEW_SAMPLE = "Hello! I am your Validate Copilot assistant. I can help you run tests, review failures, and generate new coverage for your project.";
4744
+ DEFAULT_TTS_MODEL = "speech-2.6-hd";
4745
+ DEFAULT_TTS_VOICE_ID = "English_Graceful_Lady";
4746
+ DEFAULT_TTS_SPEED = 1;
4747
+ LEGACY_TTS_VOICE_ID_MAP = {
4748
+ English_LivelyGirl: "English_PlayfulGirl",
4749
+ English_Insightful_Man: "English_Insightful_Speaker",
4750
+ English_Deep_Voice_Man: "English_ManWithDeepVoice",
4751
+ English_CasualMan: "English_Jovialman"
4752
+ };
4753
+ DEFAULT_STT_MODEL_BY_PROVIDER = {
4754
+ groq: "whisper-large-v3-turbo",
4755
+ openai: "whisper-1"
4756
+ };
4757
+ STT_PROVIDER_OPTIONS = [
4758
+ {
4759
+ id: "auto",
4760
+ label: "Auto (prefer Groq)",
4761
+ description: "Use Groq when GROQ_API_KEY is set, otherwise OpenAI Whisper."
4762
+ },
4763
+ {
4764
+ id: "groq",
4765
+ label: "Groq Whisper",
4766
+ description: "Free-tier OpenAI-compatible Whisper via Groq. Requires GROQ_API_KEY on org servers."
4767
+ },
4768
+ {
4769
+ id: "openai",
4770
+ label: "OpenAI Whisper",
4771
+ description: "OpenAI whisper-1 API. Requires OPENAI_API_KEY on org servers."
4772
+ }
4773
+ ];
4774
+ STT_MODEL_OPTIONS = [
4775
+ {
4776
+ id: "whisper-large-v3-turbo",
4777
+ label: "Whisper Large V3 Turbo",
4778
+ provider: "groq",
4779
+ description: "Fast, accurate default for Groq (recommended)."
4780
+ },
4781
+ {
4782
+ id: "whisper-large-v3",
4783
+ label: "Whisper Large V3",
4784
+ provider: "groq",
4785
+ description: "Full-size Groq Whisper model \u2014 higher quality, slower."
4786
+ },
4787
+ {
4788
+ id: "distil-whisper-large-v3-en",
4789
+ label: "Distil Whisper Large V3 (EN)",
4790
+ provider: "groq",
4791
+ description: "English-optimized distilled model \u2014 fastest Groq option."
4792
+ },
4793
+ {
4794
+ id: "whisper-1",
4795
+ label: "Whisper 1",
4796
+ provider: "openai",
4797
+ description: "OpenAI hosted Whisper model."
4798
+ }
4799
+ ];
4800
+ TTS_MODEL_OPTIONS = [
4801
+ {
4802
+ id: "speech-2.8-hd",
4803
+ label: "Speech 2.8 HD",
4804
+ provider: "minimax",
4805
+ description: "Latest high-definition model with interjection tags (recommended)."
4806
+ },
4807
+ {
4808
+ id: "speech-2.8-turbo",
4809
+ label: "Speech 2.8 Turbo",
4810
+ provider: "minimax",
4811
+ description: "Latest low-latency model with interjection tags."
4812
+ },
4813
+ {
4814
+ id: "speech-2.6-hd",
4815
+ label: "Speech 2.6 HD",
4816
+ provider: "minimax",
4817
+ description: "High-definition MiniMax voice model."
4818
+ },
4819
+ {
4820
+ id: "speech-2.6-turbo",
4821
+ label: "Speech 2.6 Turbo",
4822
+ provider: "minimax",
4823
+ description: "Lower-latency Speech 2.6 variant."
4824
+ },
4825
+ {
4826
+ id: "speech-02-hd",
4827
+ label: "Speech 02 HD",
4828
+ provider: "minimax",
4829
+ description: "Previous-generation HD model."
4830
+ },
4831
+ {
4832
+ id: "speech-02-turbo",
4833
+ label: "Speech 02 Turbo",
4834
+ provider: "minimax",
4835
+ description: "Lower-latency Speech 02 variant."
4836
+ },
4837
+ {
4838
+ id: "speech-01-hd",
4839
+ label: "Speech 01 HD",
4840
+ provider: "minimax",
4841
+ description: "Legacy HD model."
4842
+ },
4843
+ {
4844
+ id: "speech-01-turbo",
4845
+ label: "Speech 01 Turbo",
4846
+ provider: "minimax",
4847
+ description: "Legacy fast model."
4848
+ }
4849
+ ];
4850
+ TTS_VOICE_OPTIONS = [
4851
+ {
4852
+ id: "English_Graceful_Lady",
4853
+ label: "Graceful Lady",
4854
+ description: "Warm, polished professional tone.",
4855
+ gender: "female",
4856
+ accent: "English",
4857
+ sampleText: VOICE_PREVIEW_SAMPLE
4858
+ },
4859
+ {
4860
+ id: "English_ConfidentWoman",
4861
+ label: "Confident Woman",
4862
+ description: "Assured and direct \u2014 good for technical guidance.",
4863
+ gender: "female",
4864
+ accent: "English",
4865
+ sampleText: VOICE_PREVIEW_SAMPLE
4866
+ },
4867
+ {
4868
+ id: "English_SereneWoman",
4869
+ label: "Serene Woman",
4870
+ description: "Calm and measured delivery.",
4871
+ gender: "female",
4872
+ accent: "English",
4873
+ sampleText: VOICE_PREVIEW_SAMPLE
4874
+ },
4875
+ {
4876
+ id: "English_PlayfulGirl",
4877
+ label: "Playful Girl",
4878
+ description: "Upbeat and energetic.",
4879
+ gender: "female",
4880
+ accent: "English",
4881
+ sampleText: VOICE_PREVIEW_SAMPLE
4882
+ },
4883
+ {
4884
+ id: "English_CalmWoman",
4885
+ label: "Calm Woman",
4886
+ description: "Soft, reassuring cadence.",
4887
+ gender: "female",
4888
+ accent: "English",
4889
+ sampleText: VOICE_PREVIEW_SAMPLE
4890
+ },
4891
+ {
4892
+ id: "English_Insightful_Speaker",
4893
+ label: "Insightful Speaker",
4894
+ description: "Clear analyst-style narration.",
4895
+ gender: "male",
4896
+ accent: "English",
4897
+ sampleText: VOICE_PREVIEW_SAMPLE
4898
+ },
4899
+ {
4900
+ id: "English_ManWithDeepVoice",
4901
+ label: "Deep Voice Man",
4902
+ description: "Rich, authoritative baritone.",
4903
+ gender: "male",
4904
+ accent: "English",
4905
+ sampleText: VOICE_PREVIEW_SAMPLE
4906
+ },
4907
+ {
4908
+ id: "English_Jovialman",
4909
+ label: "Jovial Man",
4910
+ description: "Relaxed conversational style.",
4911
+ gender: "male",
4912
+ accent: "English",
4913
+ sampleText: VOICE_PREVIEW_SAMPLE
4914
+ },
4915
+ {
4916
+ id: "English_PatientMan",
4917
+ label: "Patient Man",
4918
+ description: "Steady, tutorial-friendly pacing.",
4919
+ gender: "male",
4920
+ accent: "English",
4921
+ sampleText: VOICE_PREVIEW_SAMPLE
4922
+ },
4923
+ {
4924
+ id: "English_FriendlyPerson",
4925
+ label: "Friendly Person",
4926
+ description: "Neutral, approachable assistant voice.",
4927
+ gender: "neutral",
4928
+ accent: "English",
4929
+ sampleText: VOICE_PREVIEW_SAMPLE
4930
+ }
4931
+ ];
4932
+ }
4933
+ });
4934
+
4550
4935
  // ../shared/dist/index.js
4551
4936
  var dist_exports = {};
4552
4937
  __export(dist_exports, {
@@ -4561,8 +4946,13 @@ __export(dist_exports, {
4561
4946
  CREDIT_PRICES_CENTS: () => CREDIT_PRICES_CENTS,
4562
4947
  DANGEROUS_DEEPLINK_SCHEMES: () => DANGEROUS_DEEPLINK_SCHEMES,
4563
4948
  DB_PRICING_PLAN_MAP: () => DB_PRICING_PLAN_MAP,
4949
+ DEFAULT_STT_MODEL_BY_PROVIDER: () => DEFAULT_STT_MODEL_BY_PROVIDER,
4950
+ DEFAULT_TTS_MODEL: () => DEFAULT_TTS_MODEL,
4951
+ DEFAULT_TTS_SPEED: () => DEFAULT_TTS_SPEED,
4952
+ DEFAULT_TTS_VOICE_ID: () => DEFAULT_TTS_VOICE_ID,
4564
4953
  DEFAULT_UI_VARIANT: () => DEFAULT_UI_VARIANT,
4565
4954
  ENVIRONMENTAL_ERROR_PREFIXES: () => ENVIRONMENTAL_ERROR_PREFIXES,
4955
+ LEGACY_TTS_VOICE_ID_MAP: () => LEGACY_TTS_VOICE_ID_MAP,
4566
4956
  MAX_CREDIT_TOPUP_CENTS: () => MAX_CREDIT_TOPUP_CENTS,
4567
4957
  MAX_DEEPLINK_LENGTH: () => MAX_DEEPLINK_LENGTH,
4568
4958
  MIN_CREDIT_TOPUP_CENTS: () => MIN_CREDIT_TOPUP_CENTS,
@@ -4584,10 +4974,15 @@ __export(dist_exports, {
4584
4974
  PLATFORM_ADMIN_EMAIL: () => PLATFORM_ADMIN_EMAIL,
4585
4975
  SELECTOR_PRIORITIES: () => SELECTOR_PRIORITIES,
4586
4976
  SHARED_PRICING_PLAN_MAP: () => SHARED_PRICING_PLAN_MAP,
4977
+ STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
4978
+ STT_PROVIDER_OPTIONS: () => STT_PROVIDER_OPTIONS,
4587
4979
  SUPPORT_EMAIL: () => SUPPORT_EMAIL,
4588
4980
  TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
4589
4981
  TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
4982
+ TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
4983
+ TTS_VOICE_OPTIONS: () => TTS_VOICE_OPTIONS,
4590
4984
  UI_VARIANTS: () => UI_VARIANTS,
4985
+ VOICE_PREVIEW_SAMPLE: () => VOICE_PREVIEW_SAMPLE,
4591
4986
  accumulateStatements: () => accumulateStatements,
4592
4987
  aiCostToCreditCents: () => aiCostToCreditCents,
4593
4988
  analyzeMobileSelectorQuality: () => analyzeMobileSelectorQuality,
@@ -4650,16 +5045,23 @@ __export(dist_exports, {
4650
5045
  isRunnableStep: () => isRunnableStep,
4651
5046
  isSafeDeepLink: () => isSafeDeepLink,
4652
5047
  isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
5048
+ isSttModelForProvider: () => isSttModelForProvider,
4653
5049
  isTestReadyForPlatform: () => isTestReadyForPlatform,
4654
5050
  isTrialExpired: () => isTrialExpired,
4655
5051
  isUIVariant: () => isUIVariant,
4656
5052
  isValidMobileAppId: () => isValidMobileAppId,
5053
+ isValidSttProvider: () => isValidSttProvider,
5054
+ isValidTtsModel: () => isValidTtsModel,
5055
+ isValidTtsVoiceId: () => isValidTtsVoiceId,
4657
5056
  mediumForMobilePlatform: () => mediumForMobilePlatform,
4658
5057
  mobileCredentialPlaceholder: () => mobileCredentialPlaceholder,
4659
5058
  mobilePlatformFromMedium: () => mobilePlatformFromMedium,
4660
5059
  normalizeControlName: () => normalizeControlName,
5060
+ normalizeTtsVoiceId: () => normalizeTtsVoiceId,
4661
5061
  otherMobilePlatform: () => otherMobilePlatform,
4662
5062
  parenDepthDelta: () => parenDepthDelta,
5063
+ pickDisplayRun: () => pickDisplayRun,
5064
+ pickLatestHealthRun: () => pickLatestHealthRun,
4663
5065
  placeholderizeRecordedMobileAction: () => placeholderizeRecordedMobileAction,
4664
5066
  placeholderizeRecordedTypeValue: () => placeholderizeRecordedTypeValue,
4665
5067
  planHasBillingPortal: () => planHasBillingPortal,
@@ -4669,7 +5071,9 @@ __export(dist_exports, {
4669
5071
  resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
4670
5072
  resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
4671
5073
  resolvePlatformLocatorBundle: () => resolvePlatformLocatorBundle,
5074
+ resolveSttModelForProvider: () => resolveSttModelForProvider,
4672
5075
  safeOrigin: () => safeOrigin,
5076
+ sanitizeSttErrorMessage: () => sanitizeSttErrorMessage,
4673
5077
  sanitizeTestVariables: () => sanitizeTestVariables,
4674
5078
  selectorCandidatesForStep: () => selectorCandidatesForStep,
4675
5079
  selectorForValue: () => selectorForValue,
@@ -4679,6 +5083,7 @@ __export(dist_exports, {
4679
5083
  stepsNeedingReviewForPlatform: () => stepsNeedingReviewForPlatform,
4680
5084
  stripEnvironmentalPrefix: () => stripEnvironmentalPrefix,
4681
5085
  stripLoginScaffolding: () => stripLoginScaffolding,
5086
+ sttModelsForProvider: () => sttModelsForProvider,
4682
5087
  summarizeHealth: () => summarizeHealth,
4683
5088
  summarizeMobileReadiness: () => summarizeMobileReadiness,
4684
5089
  summarizeRunHealth: () => summarizeRunHealth,
@@ -4703,6 +5108,7 @@ var init_dist = __esm({
4703
5108
  init_agent_env();
4704
5109
  init_test_counting();
4705
5110
  init_origin_domain();
5111
+ init_voice_config();
4706
5112
  }
4707
5113
  });
4708
5114
 
@@ -5807,6 +6213,689 @@ var require_mobile_observation = __commonJS({
5807
6213
  }
5808
6214
  });
5809
6215
 
6216
+ // ../runner-core/dist/services/mobile/mobile-exploration-state.js
6217
+ var require_mobile_exploration_state = __commonJS({
6218
+ "../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports2) {
6219
+ "use strict";
6220
+ Object.defineProperty(exports2, "__esModule", { value: true });
6221
+ exports2.MobileExplorationState = void 0;
6222
+ exports2.isDestructiveActionLabel = isDestructiveActionLabel;
6223
+ exports2.isDestructiveConfirmationInput = isDestructiveConfirmationInput;
6224
+ exports2.stableMobileControlIdentity = stableMobileControlIdentity;
6225
+ exports2.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
6226
+ exports2.deriveScreenId = deriveScreenId;
6227
+ var mobile_observation_js_1 = require_mobile_observation();
6228
+ var DEFAULT_STALE_THRESHOLD = 4;
6229
+ var MAX_SCREEN_OBSERVATIONS = 50;
6230
+ var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
6231
+ var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
6232
+ var UNSAFE_OR_COMMIT_LABEL_RE = /\b(?:submit|save|confirm|delete|remove|archive|reset|disconnect|deactivate|destroy|purge|pay|purchase|buy|checkout|place\s+order|send|post|publish|upload|import|export|sign\s*out|log\s*out|logout|accept|agree|subscribe|unsubscribe|follow|unfollow|enable|disable|book|reserve|cancel|continue|next|previous|done|apply|retry|close|dismiss|sign\s*in|log\s*in|register)\b/i;
6233
+ var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
6234
+ var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
6235
+ var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
6236
+ var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
6237
+ var SYSTEM_BACK_CONTROL_ID_RE = /^(?:action_bar_up|arrow_back|back_arrow|navigate_up|navigation_up|nav_up|toolbar_back|action_back|btn_back|button_back|back_button|back_btn|back_icon|icon_back)$/i;
6238
+ var DESTRUCTIVE_ACTION_LABEL_RE = /\b(?:deactivate|delete\s+account|delete\s+profile|close\s+account|cancel\s+account|remove\s+account|delete\s+(?:my\s+)?(?:data|history|everything|account)|delete\s+(?:all|forever|permanently)|permanently\s+(?:delete|remove|erase|deactivate)|erase\s+(?:all|everything|account|data)|wipe\s+(?:account|data|everything|all|device)|purge\s+(?:account|data|everything|all)|destroy\s+(?:account|data)|factory\s+reset|reset\s+(?:all\s+data|everything))\b/i;
6239
+ var DESTRUCTIVE_CONFIRMATION_INPUT_RE = /^(?:delete|delete\s+my\s+account|delete\s+account|delete\s+forever|permanently\s+delete|confirm\s+delete|erase|erase\s+all|wipe|deactivate|remove\s+account|close\s+account|yes,?\s*delete(?:\s+my\s+account)?)$/i;
6240
+ function isDestructiveActionLabel(label) {
6241
+ if (!label)
6242
+ return false;
6243
+ return DESTRUCTIVE_ACTION_LABEL_RE.test(normalizeControlIdentityPart(label));
6244
+ }
6245
+ function isDestructiveConfirmationInput(value) {
6246
+ if (!value)
6247
+ return false;
6248
+ return DESTRUCTIVE_CONFIRMATION_INPUT_RE.test(value.trim());
6249
+ }
6250
+ function normalizeControlIdentityPart(value) {
6251
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
6252
+ }
6253
+ function semanticControlAlias(label) {
6254
+ return `navigation-label:${normalizeControlIdentityPart(label)}`;
6255
+ }
6256
+ function localControlCoverageIdentity(screenId, identity) {
6257
+ return `screen:${screenId}:${identity}`;
6258
+ }
6259
+ function storedMobileControlIdentity(candidate) {
6260
+ return [
6261
+ candidate.identity,
6262
+ `kind:${candidate.kind}`,
6263
+ `label:${normalizeControlIdentityPart(candidate.label)}`
6264
+ ].join(":");
6265
+ }
6266
+ function isSystemBackNavigationControl(element, label) {
6267
+ if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
6268
+ return true;
6269
+ return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
6270
+ const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
6271
+ return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
6272
+ });
6273
+ }
6274
+ function stableMobileControlIdentity(element) {
6275
+ const role = normalizeControlIdentityPart(element.role || "control");
6276
+ const accessibilityId = element.accessibilityId?.trim();
6277
+ if (accessibilityId)
6278
+ return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
6279
+ const resourceId = element.resourceId?.trim();
6280
+ if (resourceId)
6281
+ return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
6282
+ const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
6283
+ if (label)
6284
+ return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
6285
+ return null;
6286
+ }
6287
+ function classifySafeMobileNavigationControl(element) {
6288
+ if (!element.actionable || element.enabled === false)
6289
+ return null;
6290
+ const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
6291
+ const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
6292
+ if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
6293
+ return null;
6294
+ if (isSystemBackNavigationControl(element, label))
6295
+ return null;
6296
+ const role = element.role.trim();
6297
+ if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
6298
+ return null;
6299
+ const linkLike = /(?:Link$|^a$)/i.test(role);
6300
+ let kind = null;
6301
+ if (/(?:TabBarItem|\btab\b)/i.test(role))
6302
+ kind = "tab";
6303
+ else if (/menuitem/i.test(role))
6304
+ kind = "menu-item";
6305
+ else if (linkLike && element.navigationProvenance)
6306
+ kind = "link";
6307
+ else if (linkLike)
6308
+ return null;
6309
+ else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
6310
+ kind = "menu-trigger";
6311
+ else if (element.expanded === false)
6312
+ kind = "disclosure";
6313
+ else if (MENU_TRIGGER_LABEL_RE.test(label))
6314
+ kind = "menu-trigger";
6315
+ else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
6316
+ kind = "modal-trigger";
6317
+ else if (element.navigationProvenance)
6318
+ kind = "navigation-button";
6319
+ else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
6320
+ kind = "navigation-button";
6321
+ if (!kind)
6322
+ return null;
6323
+ const identity = stableMobileControlIdentity(element);
6324
+ return identity ? { identity, label, kind } : null;
6325
+ }
6326
+ function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
6327
+ if (!element.navigationProvenance)
6328
+ return void 0;
6329
+ const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
6330
+ if (!durableId && !semanticFamilyId)
6331
+ return void 0;
6332
+ return [
6333
+ "shared-navigation",
6334
+ element.navigationProvenance,
6335
+ !durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
6336
+ candidate.kind,
6337
+ durableId || "semantic-only",
6338
+ normalizeControlIdentityPart(candidate.label)
6339
+ ].filter(Boolean).join(":");
6340
+ }
6341
+ function mobileNavigationRegion(element, windowSize) {
6342
+ if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
6343
+ return "unknown";
6344
+ const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
6345
+ const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
6346
+ if (centerY >= 0.72)
6347
+ return "bottom";
6348
+ if (centerY <= 0.28)
6349
+ return "top";
6350
+ if (centerX <= 0.35)
6351
+ return "left";
6352
+ if (centerX >= 0.65)
6353
+ return "right";
6354
+ return "center";
6355
+ }
6356
+ function buildSemanticNavigationGroups(elements, windowSize) {
6357
+ const groups = /* @__PURE__ */ new Map();
6358
+ for (const element of elements) {
6359
+ if (!element.navigationProvenance)
6360
+ continue;
6361
+ const candidate = classifySafeMobileNavigationControl(element);
6362
+ if (!candidate)
6363
+ continue;
6364
+ const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
6365
+ const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
6366
+ labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
6367
+ groups.set(groupKey, labels);
6368
+ }
6369
+ return new Map([...groups].filter(([, labels]) => labels.size >= 2));
6370
+ }
6371
+ function deriveScreenId(signal) {
6372
+ const namespace = signal.activity?.trim() || signal.bundleId?.trim();
6373
+ return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
6374
+ }
6375
+ function observedElementIdentity(element) {
6376
+ return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
6377
+ }
6378
+ function mergeObservedElements(previous, current) {
6379
+ const merged = [...current];
6380
+ const seen = new Set(current.map(observedElementIdentity));
6381
+ for (const element of previous) {
6382
+ const identity = observedElementIdentity(element);
6383
+ if (seen.has(identity))
6384
+ continue;
6385
+ seen.add(identity);
6386
+ merged.push(element);
6387
+ }
6388
+ return merged;
6389
+ }
6390
+ function sanitizeElementsForEvidence(elements) {
6391
+ return elements.map((element) => {
6392
+ const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
6393
+ return {
6394
+ ...element,
6395
+ ...label ? { label } : { label: void 0 },
6396
+ text: void 0
6397
+ };
6398
+ });
6399
+ }
6400
+ var MobileExplorationState = class {
6401
+ /** screenId -> ScreenState (the mobile analog of routeState). */
6402
+ screens = /* @__PURE__ */ new Map();
6403
+ /** Observed directed transitions between screens. */
6404
+ transitions = [];
6405
+ /** Transition-announced screens that do not yet have a live observation. */
6406
+ frontier = /* @__PURE__ */ new Map();
6407
+ /** Safe navigation controls observed on each screen, keyed by stable identity. */
6408
+ safeControlsByScreen = /* @__PURE__ */ new Map();
6409
+ /** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
6410
+ attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
6411
+ successfulSafeControlAttemptCount = 0;
6412
+ /** Repeated id-less structural navigation families, grouped by region. */
6413
+ semanticNavigationFamilies = /* @__PURE__ */ new Map();
6414
+ nextSemanticNavigationFamilyId = 1;
6415
+ /** The screen-id of the most recently observed screen. */
6416
+ currentScreenId = null;
6417
+ /** Latest iteration recorded (drives stale detection + firstSeen stamps). */
6418
+ iteration = 0;
6419
+ /** Stale detector: progress mark (#screens + #total elements) at last advance. */
6420
+ lastProgressMark = 0;
6421
+ /** Consecutive iterations observed with no progress. */
6422
+ staleIterations = 0;
6423
+ staleThreshold;
6424
+ constructor(options = {}) {
6425
+ this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
6426
+ }
6427
+ // -------------------------------------------------------------------------
6428
+ // Observation recording
6429
+ // -------------------------------------------------------------------------
6430
+ /**
6431
+ * Returns true when `signal` identifies a screen this run has never observed
6432
+ * before. Pure read — does not mutate state.
6433
+ */
6434
+ isNewScreen(signal) {
6435
+ return !this.screens.has(deriveScreenId(signal));
6436
+ }
6437
+ /**
6438
+ * Record an observation of `screen` at `iteration`. Creates a `ScreenState`
6439
+ * on first sight (stamping `firstSeenIteration`), otherwise refreshes the
6440
+ * stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
6441
+ * element refresh). A driver observation already contains the structural UI
6442
+ * tree, elements, screenshot association, and screen identity, so it is
6443
+ * captured evidence immediately; the model's capture_screen call only adds
6444
+ * a friendly name/type. Returns the resolved screen-id.
6445
+ */
6446
+ recordObservation(signal, screen, iteration) {
6447
+ this.iteration = iteration;
6448
+ const screenId = deriveScreenId(signal);
6449
+ const evidenceElements = sanitizeElementsForEvidence(screen.elements);
6450
+ const existing = this.screens.get(screenId);
6451
+ if (existing) {
6452
+ this.screens.set(screenId, {
6453
+ ...existing,
6454
+ elements: mergeObservedElements(existing.elements, evidenceElements)
6455
+ });
6456
+ } else {
6457
+ this.screens.set(screenId, {
6458
+ screenId,
6459
+ firstSeenIteration: iteration,
6460
+ elements: evidenceElements,
6461
+ visited: true
6462
+ });
6463
+ }
6464
+ this.frontier.delete(screenId);
6465
+ const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
6466
+ const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
6467
+ const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
6468
+ for (const [groupKey, labels] of semanticNavigationGroups) {
6469
+ semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
6470
+ }
6471
+ for (const element of evidenceElements) {
6472
+ const candidate = classifySafeMobileNavigationControl(element);
6473
+ if (!candidate)
6474
+ continue;
6475
+ const storedIdentity = storedMobileControlIdentity(candidate);
6476
+ const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
6477
+ controls.set(storedIdentity, {
6478
+ ...candidate,
6479
+ identity: storedIdentity,
6480
+ sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
6481
+ });
6482
+ }
6483
+ if (controls.size > 0)
6484
+ this.safeControlsByScreen.set(screenId, controls);
6485
+ this.currentScreenId = screenId;
6486
+ this.updateStaleProgress();
6487
+ return screenId;
6488
+ }
6489
+ /**
6490
+ * Back-compatible acknowledgement from capture_screen. Observation already
6491
+ * marks structural evidence visited; this call only resolves a rare pending
6492
+ * transition frontier and preserves callers that annotate explicitly.
6493
+ */
6494
+ markVisited(screenId) {
6495
+ const state2 = this.screens.get(screenId);
6496
+ if (state2 && !state2.visited) {
6497
+ this.screens.set(screenId, { ...state2, visited: true });
6498
+ }
6499
+ this.frontier.delete(screenId);
6500
+ }
6501
+ /**
6502
+ * Record a confirmed tap against a safe navigation candidate. Callers invoke
6503
+ * this only after the driver reports success; recovery-unknown, stale-ref, and
6504
+ * failed actions must remain on the breadth frontier.
6505
+ */
6506
+ noteControlAttempt(screenId, element) {
6507
+ if (!screenId)
6508
+ return false;
6509
+ const candidate = classifySafeMobileNavigationControl(element);
6510
+ const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
6511
+ if (candidate) {
6512
+ const storedIdentity = storedMobileControlIdentity(candidate);
6513
+ const existing = controls.get(storedIdentity);
6514
+ controls.set(storedIdentity, {
6515
+ ...candidate,
6516
+ identity: storedIdentity,
6517
+ sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
6518
+ });
6519
+ this.safeControlsByScreen.set(screenId, controls);
6520
+ }
6521
+ const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
6522
+ if (!tappedLabel)
6523
+ return false;
6524
+ const before = this.attemptedSafeControlCoverageIdentities.size;
6525
+ const alias = semanticControlAlias(tappedLabel);
6526
+ for (const control of controls.values()) {
6527
+ if (semanticControlAlias(control.label) === alias) {
6528
+ this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
6529
+ if (control.sharedCoverageIdentity) {
6530
+ this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
6531
+ }
6532
+ }
6533
+ }
6534
+ if (this.attemptedSafeControlCoverageIdentities.size > before) {
6535
+ this.successfulSafeControlAttemptCount += 1;
6536
+ this.staleIterations = 0;
6537
+ return true;
6538
+ }
6539
+ return false;
6540
+ }
6541
+ /** Attach a human-friendly name / coarse type to a screen (capture_screen). */
6542
+ annotateScreen(screenId, annotation) {
6543
+ const state2 = this.screens.get(screenId);
6544
+ if (!state2)
6545
+ return;
6546
+ this.screens.set(screenId, {
6547
+ ...state2,
6548
+ screenName: annotation.screenName ?? state2.screenName,
6549
+ screenType: annotation.screenType ?? state2.screenType
6550
+ });
6551
+ }
6552
+ /** Attach structurally observed authentication provenance to a screen. */
6553
+ setScreenAuthEvidence(screenId, evidence) {
6554
+ const state2 = this.screens.get(screenId);
6555
+ if (!state2)
6556
+ return;
6557
+ this.screens.set(screenId, {
6558
+ ...state2,
6559
+ requiresAuth: evidence.requiresAuth,
6560
+ authProvenance: evidence.authProvenance,
6561
+ sessionState: { ...evidence.sessionState }
6562
+ });
6563
+ }
6564
+ /** Append a free-form observation note to a screen (observations tool). */
6565
+ addScreenObservation(screenId, note) {
6566
+ const state2 = this.screens.get(screenId);
6567
+ if (!state2)
6568
+ return;
6569
+ const normalized = note.trim();
6570
+ if (!normalized)
6571
+ return;
6572
+ const current = state2.observations ?? [];
6573
+ if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
6574
+ return;
6575
+ }
6576
+ const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
6577
+ this.screens.set(screenId, { ...state2, observations });
6578
+ }
6579
+ /**
6580
+ * Record a directed transition between two screens. Both endpoints are
6581
+ * ensured to exist on the frontier if not yet observed (mirrors the web
6582
+ * reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
6583
+ * `toScreenId` becomes the current screen when the transition starts from the
6584
+ * current screen.
6585
+ */
6586
+ recordTransition(transition) {
6587
+ let recordedTransition = transition;
6588
+ if (!transition.viaControlIdentity && transition.viaRef) {
6589
+ const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
6590
+ if (sourceElement) {
6591
+ recordedTransition = {
6592
+ ...transition,
6593
+ viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
6594
+ };
6595
+ }
6596
+ }
6597
+ const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
6598
+ if (!alreadyRecorded) {
6599
+ this.transitions.push(recordedTransition);
6600
+ }
6601
+ for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
6602
+ if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
6603
+ this.frontier.set(endpointId, {
6604
+ screenId: endpointId,
6605
+ source: "transition",
6606
+ discoveredAtIteration: this.iteration
6607
+ });
6608
+ }
6609
+ }
6610
+ if (transition.fromScreenId === this.currentScreenId) {
6611
+ this.currentScreenId = transition.toScreenId;
6612
+ }
6613
+ }
6614
+ // -------------------------------------------------------------------------
6615
+ // Selectors
6616
+ // -------------------------------------------------------------------------
6617
+ /** The current screen-id (most recently observed), or null before any observation. */
6618
+ getCurrentScreenId() {
6619
+ return this.currentScreenId;
6620
+ }
6621
+ /** ScreenState for `screenId`, if known. */
6622
+ getScreen(screenId) {
6623
+ return this.screens.get(screenId);
6624
+ }
6625
+ /** Number of distinct screens observed this run. */
6626
+ getScreenCount() {
6627
+ return this.screens.size;
6628
+ }
6629
+ /** Number of screens backed by a live structural driver observation. */
6630
+ getVisitedScreenCount() {
6631
+ let total = 0;
6632
+ for (const state2 of this.screens.values()) {
6633
+ if (state2.visited)
6634
+ total += 1;
6635
+ }
6636
+ return total;
6637
+ }
6638
+ /** Total elements observed across all known screens (latest snapshot each). */
6639
+ getTotalElementCount() {
6640
+ let total = 0;
6641
+ for (const state2 of this.screens.values()) {
6642
+ total += state2.elements.length;
6643
+ }
6644
+ return total;
6645
+ }
6646
+ /**
6647
+ * Frontier screen-ids ranked by recency (most recently discovered first),
6648
+ * excluding any screen already visited. Mirrors the web `getRankedFrontier`
6649
+ * intent (filter visited, rank by recency) — without URL prefix similarity,
6650
+ * which has no mobile analog.
6651
+ */
6652
+ getFrontier() {
6653
+ return [...this.frontier.values()].filter((item) => {
6654
+ const state2 = this.screens.get(item.screenId);
6655
+ return !state2 || !state2.visited;
6656
+ }).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
6657
+ }
6658
+ /** Safe navigation candidates not yet attempted, deduped across shared chrome. */
6659
+ getUnexploredSafeNavigationCandidates() {
6660
+ const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
6661
+ const candidates = [];
6662
+ for (const [screenId, controls] of this.safeControlsByScreen) {
6663
+ const origin = this.screens.get(screenId);
6664
+ const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
6665
+ if (authenticatedProductObserved && originIsConfirmedPublicEntry)
6666
+ continue;
6667
+ for (const control of controls.values()) {
6668
+ const alias = semanticControlAlias(control.label);
6669
+ const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
6670
+ if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
6671
+ continue;
6672
+ const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
6673
+ if (existing) {
6674
+ existing.screenIds.add(screenId);
6675
+ } else {
6676
+ candidates.push({
6677
+ identity: coverageIdentity,
6678
+ label: control.label,
6679
+ kind: control.kind,
6680
+ alias,
6681
+ screenIds: /* @__PURE__ */ new Set([screenId])
6682
+ });
6683
+ }
6684
+ }
6685
+ }
6686
+ return candidates.map((candidate) => ({
6687
+ identity: candidate.identity,
6688
+ label: candidate.label,
6689
+ kind: candidate.kind,
6690
+ screenIds: [...candidate.screenIds]
6691
+ })).sort((a, b) => {
6692
+ const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
6693
+ const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
6694
+ if (aCurrent !== bCurrent)
6695
+ return aCurrent ? -1 : 1;
6696
+ return b.screenIds.length - a.screenIds.length;
6697
+ });
6698
+ }
6699
+ /** Harness-owned survey completion floor, independent of model assertions. */
6700
+ getSurveyBreadthCoverage() {
6701
+ const unvisitedScreenIds = this.getFrontier();
6702
+ const unexploredControls = this.getUnexploredSafeNavigationCandidates();
6703
+ const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
6704
+ const missing = [];
6705
+ if (unvisitedScreenIds.length > 0) {
6706
+ missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
6707
+ }
6708
+ if (unexploredControls.length > 0) {
6709
+ missing.push(`${unexploredControls.length} safe navigation control(s)`);
6710
+ }
6711
+ return {
6712
+ required: true,
6713
+ complete,
6714
+ unvisitedScreenIds,
6715
+ unexploredControls,
6716
+ attemptedControlCount: this.successfulSafeControlAttemptCount,
6717
+ ...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
6718
+ };
6719
+ }
6720
+ /**
6721
+ * Monotonic structural progress token for adaptive survey leases. Frontier
6722
+ * cardinality alone is insufficient: attempting control A can reveal control
6723
+ * B and leave the remaining count unchanged even though real work happened.
6724
+ */
6725
+ getSurveyBreadthProgressMark() {
6726
+ const discoveredControlIdentities = /* @__PURE__ */ new Set();
6727
+ for (const [screenId, controls] of this.safeControlsByScreen) {
6728
+ for (const control of controls.values()) {
6729
+ discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
6730
+ }
6731
+ }
6732
+ return [
6733
+ `screens:${this.screens.size}`,
6734
+ `attempts:${this.successfulSafeControlAttemptCount}`,
6735
+ `controls:${discoveredControlIdentities.size}`,
6736
+ `transitions:${this.transitions.length}`
6737
+ ].join("|");
6738
+ }
6739
+ resolveSemanticNavigationFamilyId(groupKey, labels) {
6740
+ const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
6741
+ let best;
6742
+ let bestIntersection = 0;
6743
+ for (const family of families) {
6744
+ let intersection = 0;
6745
+ for (const label of labels) {
6746
+ if (family.labels.has(label))
6747
+ intersection += 1;
6748
+ }
6749
+ const overlap = intersection / Math.min(labels.size, family.labels.size);
6750
+ if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
6751
+ best = family;
6752
+ bestIntersection = intersection;
6753
+ }
6754
+ }
6755
+ if (best) {
6756
+ for (const label of labels)
6757
+ best.labels.add(label);
6758
+ return best.id;
6759
+ }
6760
+ const created = {
6761
+ id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
6762
+ labels: new Set(labels)
6763
+ };
6764
+ families.push(created);
6765
+ this.semanticNavigationFamilies.set(groupKey, families);
6766
+ return created.id;
6767
+ }
6768
+ /** Observed transitions (defensive copy). */
6769
+ getTransitions() {
6770
+ return [...this.transitions];
6771
+ }
6772
+ // -------------------------------------------------------------------------
6773
+ // Stale-progress detection (ported semantics from the web stale guard)
6774
+ // -------------------------------------------------------------------------
6775
+ /**
6776
+ * Progress mark: distinct screens weighted heavily, plus total elements seen.
6777
+ * A run advances this mark whenever it finds a new screen OR new elements;
6778
+ * the stale detector triggers when the mark stops advancing for
6779
+ * `staleThreshold` consecutive iterations.
6780
+ */
6781
+ getProgressMark() {
6782
+ return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
6783
+ }
6784
+ updateStaleProgress() {
6785
+ const mark = this.getProgressMark();
6786
+ if (mark > this.lastProgressMark) {
6787
+ this.lastProgressMark = mark;
6788
+ this.staleIterations = 0;
6789
+ } else {
6790
+ this.staleIterations += 1;
6791
+ }
6792
+ }
6793
+ /** Consecutive no-progress iterations observed so far. */
6794
+ getStaleIterations() {
6795
+ return this.staleIterations;
6796
+ }
6797
+ /**
6798
+ * True when the run has gone `staleThreshold` consecutive iterations with no
6799
+ * new screen and no new element — the signal the explorer uses to nudge or
6800
+ * finish (mirrors the web explorer's stale guard).
6801
+ */
6802
+ isStaleProgress() {
6803
+ return this.staleIterations >= this.staleThreshold;
6804
+ }
6805
+ // -------------------------------------------------------------------------
6806
+ // Planner evidence export
6807
+ // -------------------------------------------------------------------------
6808
+ /**
6809
+ * Snapshot the screen-graph as a plain object (frozen mobile-types
6810
+ * `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
6811
+ * state.
6812
+ */
6813
+ toScreenGraph() {
6814
+ const screens = /* @__PURE__ */ new Map();
6815
+ for (const [id, state2] of this.screens) {
6816
+ screens.set(id, {
6817
+ ...state2,
6818
+ elements: [...state2.elements],
6819
+ observations: state2.observations ? [...state2.observations] : void 0,
6820
+ sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
6821
+ });
6822
+ }
6823
+ return {
6824
+ screens,
6825
+ transitions: this.transitions.map((t) => ({
6826
+ fromScreenId: t.fromScreenId,
6827
+ toScreenId: t.toScreenId,
6828
+ viaRef: t.viaRef,
6829
+ viaControlIdentity: t.viaControlIdentity,
6830
+ action: t.action
6831
+ }))
6832
+ };
6833
+ }
6834
+ /**
6835
+ * Build the planner evidence shape: one `CollectedPageData` per screen, with
6836
+ * the screen-id carried in BOTH `route` (the on-the-wire positional field the
6837
+ * server keys screenshots by) AND the explicit `screenId` field (per the
6838
+ * Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
6839
+ * the screen's parsed actionable elements into the platform-neutral
6840
+ * `CollectedPageElement[]` shape the server persists as SiteElements — without
6841
+ * this, mobile screens land with zero elements (coverage always 0%, no element
6842
+ * intelligence). `triggersNavigation` is derived from the durable source
6843
+ * element identity captured when the transition happened (with ref-only
6844
+ * fallback for legacy transitions). Insertion order matches first-seen order.
6845
+ */
6846
+ buildInMemoryScreenMap() {
6847
+ const pages = [];
6848
+ const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
6849
+ const navRefsByScreen = /* @__PURE__ */ new Map();
6850
+ const navIdentitiesByScreen = /* @__PURE__ */ new Map();
6851
+ for (const transition of this.transitions) {
6852
+ if (transition.viaControlIdentity) {
6853
+ const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
6854
+ set.add(transition.viaControlIdentity);
6855
+ navIdentitiesByScreen.set(transition.fromScreenId, set);
6856
+ } else if (transition.viaRef) {
6857
+ const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
6858
+ set.add(transition.viaRef);
6859
+ navRefsByScreen.set(transition.fromScreenId, set);
6860
+ }
6861
+ }
6862
+ for (const state2 of ordered) {
6863
+ const page = {
6864
+ route: state2.screenId,
6865
+ screenId: state2.screenId,
6866
+ pageType: state2.screenType ?? "UNKNOWN",
6867
+ elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
6868
+ navigationRefs: navRefsByScreen.get(state2.screenId),
6869
+ navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
6870
+ }),
6871
+ requiresAuth: state2.requiresAuth ?? false
6872
+ };
6873
+ if (state2.sessionState) {
6874
+ page.sessionState = { ...state2.sessionState };
6875
+ }
6876
+ if (state2.screenName) {
6877
+ page.title = state2.screenName;
6878
+ }
6879
+ if (state2.observations && state2.observations.length > 0) {
6880
+ page.observations = state2.observations.map((note) => ({
6881
+ action: "observe",
6882
+ outcome: note,
6883
+ outcomeType: "SUCCESS"
6884
+ }));
6885
+ }
6886
+ pages.push(page);
6887
+ }
6888
+ return pages;
6889
+ }
6890
+ /** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
6891
+ toScreenMap() {
6892
+ return this.buildInMemoryScreenMap();
6893
+ }
6894
+ };
6895
+ exports2.MobileExplorationState = MobileExplorationState;
6896
+ }
6897
+ });
6898
+
5810
6899
  // ../runner-core/dist/services/mobile/mobile-keyboard.js
5811
6900
  var require_mobile_keyboard = __commonJS({
5812
6901
  "../runner-core/dist/services/mobile/mobile-keyboard.js"(exports2) {
@@ -63525,6 +64614,7 @@ var require_mobile_explorer = __commonJS({
63525
64614
  var ai_retry_js_1 = require_ai_retry();
63526
64615
  var mobile_source_parser_js_1 = require_mobile_source_parser();
63527
64616
  var mobile_observation_js_1 = require_mobile_observation();
64617
+ var mobile_exploration_state_js_1 = require_mobile_exploration_state();
63528
64618
  var mobile_boundary_js_1 = require_mobile_boundary();
63529
64619
  var mobile_keyboard_js_1 = require_mobile_keyboard();
63530
64620
  var mobile_auth_coverage_js_1 = require_mobile_auth_coverage();
@@ -63882,6 +64972,10 @@ var require_mobile_explorer = __commonJS({
63882
64972
  return void 0;
63883
64973
  return normalized;
63884
64974
  }
64975
+ function destructiveActionRefusal(label, kind = "tap") {
64976
+ const what = kind === "confirmation" ? `Typing "${label}" is a destructive confirmation` : `Tapping "${label}" is an irreversible, high-stakes destructive action`;
64977
+ return `Refused: ${what} (account/data deletion, deactivation, wipe, or similar). Autonomous testing must never execute irreversible destruction on the account under test. Record it as an untested destructive flow in your summary and choose a non-destructive control instead.`;
64978
+ }
63885
64979
  function hybridElementForEvidence(element, credentials) {
63886
64980
  const id = safeHybridIdentity(element.id, credentials);
63887
64981
  const name = safeHybridIdentity(element.name, credentials);
@@ -64504,7 +65598,7 @@ Max iterations: ${maxIterations}. You will be warned near the end.`;
64504
65598
  3. After each navigation, capture_screen the destination and record_transition. After completing a meaningful flow (login, search, create), ${enableRecordJourney ? "call record_journey with the steps and expected outcome." : "continue exploring."}
64505
65599
  4. Use mobile_relaunch for a clean, hermetic restart before testing a distinct flow that needs fresh state.
64506
65600
  5. mobile_swipe to scroll; mobile_back to return.
64507
- 6. Do NOT trigger irreversible side effects (real payments, sending messages/invites to others). Note them instead.
65601
+ 6. Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages/invites to others; and ESPECIALLY account/data deletion, deactivation, "delete account", factory reset, or any "type DELETE to confirm" gate. The runner hard-blocks destructive taps and confirmation keystrokes, so attempting them only wastes an iteration. Note such flows in your summary and explore elsewhere.
64508
65602
  7. Stay inside this app.${targetLine} If a tap opens an app store, settings, browser, or another app, do not explore it; return with mobile_back or mobile_relaunch.
64509
65603
 
64510
65604
  Max iterations: ${maxIterations}. You will be warned near the end.`;
@@ -65369,6 +66463,11 @@ Exploration complete: ${summary}`);
65369
66463
  if (!driver.webTap) {
65370
66464
  return { ok: false, error: "This driver does not support hybrid WEBVIEW taps." };
65371
66465
  }
66466
+ const hybridTapLabel = hybridElementForEvidence(hybrid, redactionCredentials).label ?? "";
66467
+ if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(hybridTapLabel)) {
66468
+ log2(` \u26A0 Refused destructive tap on "${hybridTapLabel}" (account/data-deletion class); noting instead.`);
66469
+ return { ok: false, error: destructiveActionRefusal(hybridTapLabel) };
66470
+ }
65372
66471
  const authSurfaceBeforeTap2 = currentAuthSurface();
65373
66472
  const fromScreenId2 = state2.getCurrentScreenId();
65374
66473
  const actionResult2 = await driver.webTap(hybrid.webviewContext, hybrid.css);
@@ -65399,6 +66498,11 @@ Exploration complete: ${summary}`);
65399
66498
  if (!resolved) {
65400
66499
  return { ok: false, error: `Unknown element ref "${String(toolArgs.ref)}". Call mobile_snapshot first; refs are only valid for the latest snapshot.` };
65401
66500
  }
66501
+ const nativeTapLabel = resolved.element.label ?? resolved.element.text ?? resolved.element.accessibilityId ?? "";
66502
+ if ((0, mobile_exploration_state_js_1.isDestructiveActionLabel)(nativeTapLabel)) {
66503
+ log2(` \u26A0 Refused destructive tap on "${nativeTapLabel}" (account/data-deletion class); noting instead.`);
66504
+ return { ok: false, error: destructiveActionRefusal(nativeTapLabel) };
66505
+ }
65402
66506
  const fromScreenId = state2.getCurrentScreenId();
65403
66507
  const boundaryBeforeAction = currentBoundary();
65404
66508
  const startedOutsideTarget = boundaryBeforeAction.kind !== "target" && boundaryBeforeAction.kind !== "unknown";
@@ -65439,6 +66543,10 @@ Exploration complete: ${summary}`);
65439
66543
  // ----- Type -----
65440
66544
  case "mobile_type": {
65441
66545
  const value = typeof toolArgs.value === "string" ? toolArgs.value : String(toolArgs.value ?? "");
66546
+ if ((0, mobile_exploration_state_js_1.isDestructiveConfirmationInput)(value)) {
66547
+ log2(` \u26A0 Refused destructive confirmation input "${value}"; noting instead.`);
66548
+ return { ok: false, error: destructiveActionRefusal(value, "confirmation") };
66549
+ }
65442
66550
  const allowedLiteral = inbox ? inbox.state.email : "";
65443
66551
  for (const [key, secret] of Object.entries(credentials)) {
65444
66552
  if (!secret || secret.length < 4)
@@ -70148,7 +71256,7 @@ var require_mobile_generator = __commonJS({
70148
71256
  - Re-snapshot before every assertion so the ref is fresh.
70149
71257
  - Assert the OUTCOME of the flow (the new screen / new content), not the control you just tapped.
70150
71258
  - Assert only DURABLE elements \u2014 screen chrome that exists on every visit (tab labels, headers, nav bars, the entity you just created). NEVER assert one-time / data-dependent UI: empty-state CTAs, first-run or onboarding prompts, or counters \u2014 they vanish as soon as the account has data, and the test then fails forever on a healthy app.
70151
- - Do NOT trigger irreversible side effects (real payments, sending messages to others) \u2014 note them and assert what you safely can.
71259
+ - Do NOT trigger irreversible or destructive side effects \u2014 real payments; sending messages to others; and ESPECIALLY account/data deletion, deactivation, or any "type DELETE to confirm" gate (the runner hard-blocks destructive taps and confirmation keystrokes). Note them and assert what you safely can.
70152
71260
  - Stay inside this app.${targetLine} Do not open or use app stores, browsers, email apps, settings, or any other app. If an action leaves the target app, the runner will relaunch the target app and reject that action.
70153
71261
  - NEVER tap the phone's home screen / launcher (app icons, "Predicted app" chips, search bars, widgets). Those are NOT part of the app under test \u2014 tapping them is junk that poisons the test. If the app ever backgrounds or you find yourself on the launcher, use mobile_relaunch to cold-start the app; do NOT tap a launcher icon or app-prediction to get back in.
70154
71262
  - Do NOT re-do a sequence you already completed. If you filled a form then navigated away, do NOT come back and fill it again \u2014 the scenario needs ONE clean pass. Filling the login form, going back, and re-filling it is exactly the kind of wandering that produces a bloated, broken test.
@@ -78821,6 +79929,34 @@ var require_anchor_harvester = __commonJS({
78821
79929
  }
78822
79930
  });
78823
79931
 
79932
+ // ../runner-core/dist/services/discovery-lint-flags.js
79933
+ var require_discovery_lint_flags = __commonJS({
79934
+ "../runner-core/dist/services/discovery-lint-flags.js"(exports2) {
79935
+ "use strict";
79936
+ Object.defineProperty(exports2, "__esModule", { value: true });
79937
+ exports2.structuralLocatorLintMode = structuralLocatorLintMode;
79938
+ exports2.isPwMethodPreflightEnabled = isPwMethodPreflightEnabled;
79939
+ exports2.isVqaLoginDedupeEnabled = isVqaLoginDedupeEnabled;
79940
+ function strictOn(value) {
79941
+ return value === "true" || value === "1";
79942
+ }
79943
+ function structuralLocatorLintMode() {
79944
+ const v = (process.env.DISCOVERY_STRUCTURAL_LOCATOR_LINT || "").trim().toLowerCase();
79945
+ if (v === "warn")
79946
+ return "warn";
79947
+ if (v === "true" || v === "1" || v === "enforce")
79948
+ return "enforce";
79949
+ return "off";
79950
+ }
79951
+ function isPwMethodPreflightEnabled() {
79952
+ return strictOn(process.env.DISCOVERY_PW_METHOD_PREFLIGHT);
79953
+ }
79954
+ function isVqaLoginDedupeEnabled() {
79955
+ return strictOn(process.env.DISCOVERY_VQALOGIN_DEDUPE);
79956
+ }
79957
+ }
79958
+ });
79959
+
78824
79960
  // ../runner-core/dist/services/auth-recipe.js
78825
79961
  var require_auth_recipe = __commonJS({
78826
79962
  "../runner-core/dist/services/auth-recipe.js"(exports2) {
@@ -78843,9 +79979,11 @@ var require_auth_recipe = __commonJS({
78843
79979
  exports2.extractAuthRegion = extractAuthRegion;
78844
79980
  exports2.stripAuthRegions = stripAuthRegions;
78845
79981
  exports2.ensureExpectImported = ensureExpectImported;
79982
+ exports2.ensureLoginCall = ensureLoginCall;
78846
79983
  exports2.upsertAuthBlock = upsertAuthBlock;
78847
79984
  exports2.buildHealAuthStamp = buildHealAuthStamp;
78848
79985
  var crypto_1 = require("crypto");
79986
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
78849
79987
  exports2.AUTH_MARKER_VERSION = "v1";
78850
79988
  var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
78851
79989
  var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
@@ -79109,10 +80247,13 @@ ${tail}`;
79109
80247
  var TEST_OPENER_RE = /test(?:\.(?:only|skip|fixme))?\s*\(\s*(['"`])(?:[^\\]|\\.)*?\1\s*,\s*(?:\{[\s\S]*?\}\s*,\s*)?async\s*\(\s*\{[^}]*\}[^)]*\)\s*=>\s*\{/;
79110
80248
  var LOGIN_CALL_RE = /await\s+__vqaLogin\s*\(\s*page\s*\)/;
79111
80249
  function ensureLoginCall(code) {
79112
- const matches = [...code.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
79113
- if (matches.length === 0)
80250
+ if (code.match(new RegExp(TEST_OPENER_RE.source, "g")) === null)
79114
80251
  return code;
79115
80252
  let result = code;
80253
+ if ((0, discovery_lint_flags_js_1.isVqaLoginDedupeEnabled)()) {
80254
+ result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
80255
+ }
80256
+ const matches = [...result.matchAll(new RegExp(TEST_OPENER_RE.source, "g"))];
79116
80257
  for (let i = matches.length - 1; i >= 0; i--) {
79117
80258
  const m = matches[i];
79118
80259
  if (m.index === void 0)
@@ -79521,7 +80662,7 @@ var require_agentic_discovery_prompts = __commonJS({
79521
80662
  "## Test-quality bar",
79522
80663
  "",
79523
80664
  "- ONE `test.describe(<featureName>)` block per file with ONE `test(...)` call inside.",
79524
- "- Use `await page.goto(<relativePath>)` \u2014 baseURL is set in the runner config; never hardcode the host.",
80665
+ "- Use `await page.goto(<relativePath>)` \u2014 baseURL is set in the runner config; never hardcode the host. If the base URL includes a path (e.g. `https://host/app`), every goto path MUST start with that path: Playwright only prefixes relative paths, so `page.goto('/')` lands on the ORIGIN ROOT outside the app \u2014 the app home is `page.goto('/app')`.",
79525
80666
  "- Every test must contain at least one `expect(...)` assertion.",
79526
80667
  "- **The LAST statement inside the test body MUST be `await expect(...)` that PROVES THE OUTCOME of the test's primary action.** A test ending in `.click()`, `.fill()`, `.press()`, or a `console.log` is INVALID. For an INTERACTION test, re-asserting that the control you just clicked is visible or `aria-selected=true` proves NOTHING (the toggle could be broken and the test would still pass) and is rejected by the assertion-strength gate. Instead verify the post-action state: a before\u2192after count/value change, new content/value that appeared (`getByText` of the revealed value, `toHaveText`/`toContainText`), a created item present or a removed item gone (`toHaveCount`/`not.toBeVisible`), a field value (`toHaveValue`), or a control now disabled (`toBeDisabled`). EXEMPT: a pure navigation/routing test may end on `toHaveURL` + one landmark `toBeVisible`, and the mandatory post-login wait ends on a landmark `toBeVisible` \u2014 those are correct and required. The parent validator REJECTS tests whose last non-trivial line is anything other than `await expect(...)`.",
79527
80668
  " Prefer: `expect(locator).toBeVisible()`, `expect(page).toHaveURL(/regex/)`, `expect(locator).toHaveText(...)`.",
@@ -81757,7 +82898,7 @@ var require_agentic_discovery_prompts = __commonJS({
81757
82898
  lines.push("## Test-quality bar");
81758
82899
  lines.push("");
81759
82900
  lines.push("- ONE `test.describe(<featureName>)` block per file with ONE `test(...)` inside.");
81760
- lines.push("- Use `await page.goto(<relativePath>)` \u2014 never hardcode the host.");
82901
+ lines.push("- Use `await page.goto(<relativePath>)` \u2014 never hardcode the host. If the base URL includes a path (e.g. `https://host/app`), every goto path MUST start with that path (`page.goto('/')` escapes to the origin root \u2014 the app home is `page.goto('/app')`).");
81761
82902
  lines.push("- Every test MUST include the `@generated` tag.");
81762
82903
  lines.push("- Each test creates its OWN run-scoped data; never depend on pre-seeded data.");
81763
82904
  lines.push("");
@@ -82311,6 +83452,21 @@ var require_agentic_discovery_prompts = __commonJS({
82311
83452
  lines.push(`Use \`browser_navigate\` with the full URL (prepend ${baseUrl}). In the`);
82312
83453
  lines.push("emitted test code keep URLs RELATIVE (`page.goto('/path')`) \u2014 the Playwright");
82313
83454
  lines.push("config sets `baseURL`.");
83455
+ {
83456
+ const gotoBasePath = (() => {
83457
+ try {
83458
+ const p = new URL(baseUrl).pathname.replace(/\/+$/, "");
83459
+ return p && p !== "/" ? p : void 0;
83460
+ } catch {
83461
+ return void 0;
83462
+ }
83463
+ })();
83464
+ if (gotoBasePath) {
83465
+ lines.push(`IMPORTANT: the base URL has a PATH (\`${gotoBasePath}\`) and a leading-slash path escapes`);
83466
+ lines.push(`it to the origin root. Every \`page.goto\` path MUST start with \`${gotoBasePath}\` \u2014 the`);
83467
+ lines.push(`app home is \`page.goto('${gotoBasePath}')\`, NOT \`page.goto('/')\`.`);
83468
+ }
83469
+ }
82314
83470
  lines.push("");
82315
83471
  lines.push("## The scenario");
82316
83472
  lines.push("");
@@ -82989,17 +84145,19 @@ var require_discover_generator = __commonJS({
82989
84145
  "../runner-core/dist/services/discover-generator.js"(exports2) {
82990
84146
  "use strict";
82991
84147
  Object.defineProperty(exports2, "__esModule", { value: true });
82992
- exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
84148
+ exports2.MIN_BUG_DESCRIPTION_LENGTH = exports2.VALID_PLAYWRIGHT_GETBY_METHODS = exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = void 0;
82993
84149
  exports2.filterMcpToolsForScenarioGen = filterMcpToolsForScenarioGen;
82994
84150
  exports2.detectScenarioSignals = detectScenarioSignals;
82995
84151
  exports2.scenarioStepsCoverAuth = scenarioStepsCoverAuth;
82996
84152
  exports2.getLoginCommands = getLoginCommands;
84153
+ exports2.hasConfiguredLoginCredentials = hasConfiguredLoginCredentials;
82997
84154
  exports2.computeSkeletonTags = computeSkeletonTags;
82998
84155
  exports2.shouldReplayAuthRecipe = shouldReplayAuthRecipe;
82999
84156
  exports2.buildPageContext = buildPageContext;
83000
84157
  exports2.stripTypescriptSyntax = stripTypescriptSyntax;
83001
84158
  exports2.stripImportDeclarations = stripImportDeclarations;
83002
84159
  exports2.stripStringsAndComments = stripStringsAndComments;
84160
+ exports2.gotoBasePathFromBaseUrl = gotoBasePathFromBaseUrl;
83003
84161
  exports2.sanitizeCode = sanitizeCode;
83004
84162
  exports2.validateTests = validateTests;
83005
84163
  exports2.formatEvidenceForLiveGenerate = formatEvidenceForLiveGenerate;
@@ -83010,6 +84168,7 @@ var require_discover_generator = __commonJS({
83010
84168
  exports2.upgradePositionalProvenLines = upgradePositionalProvenLines;
83011
84169
  exports2.synthesizeRefActionLines = synthesizeRefActionLines;
83012
84170
  exports2.renderSessionRecording = renderSessionRecording;
84171
+ exports2.scanFragileStructuralLocators = scanFragileStructuralLocators;
83013
84172
  exports2.validateFinalActionFidelity = validateFinalActionFidelity;
83014
84173
  exports2.resolveSuspectedBugVerdict = resolveSuspectedBugVerdict;
83015
84174
  exports2.buildScenarioSystemPrompt = buildScenarioSystemPrompt;
@@ -83037,6 +84196,7 @@ var require_discover_generator = __commonJS({
83037
84196
  var auth_recipe_js_1 = require_auth_recipe();
83038
84197
  var auth_replay_js_1 = require_auth_replay();
83039
84198
  var agentic_discovery_prompts_js_1 = require_agentic_discovery_prompts();
84199
+ var discovery_lint_flags_js_1 = require_discovery_lint_flags();
83040
84200
  exports2.BLOCKED_MCP_TOOLS_SCENARIO_GEN = /* @__PURE__ */ new Set([
83041
84201
  "browser_install",
83042
84202
  "browser_close",
@@ -83586,11 +84746,15 @@ var require_discover_generator = __commonJS({
83586
84746
  const replayPlan = (0, auth_recipe_js_1.buildReplayPlan)(login);
83587
84747
  return { ...login, replayPlan };
83588
84748
  }
83589
- function computeSkeletonTags(targetPages, pages) {
84749
+ function hasConfiguredLoginCredentials(credentials) {
84750
+ return ["TEST_USER_EMAIL", "TEST_USER_PASSWORD"].some((k) => typeof credentials?.[k] === "string" && credentials[k].trim().length > 0);
84751
+ }
84752
+ function computeSkeletonTags(targetPages, pages, opts) {
83590
84753
  const isAuthPage = (p) => p.pageType === "AUTH" || /\/(auth|login|sign-?in)\b/i.test(p.route);
84754
+ const siteHasAuthEvidence = pages.some(isAuthPage) || opts?.extraAuthEvidence === true;
83591
84755
  const targetPagesInSiteMap = pages.filter((p) => targetPages.includes(p.route));
83592
84756
  const hasAuthPage = targetPagesInSiteMap.some(isAuthPage);
83593
- const hasAuthRequiredPage = targetPagesInSiteMap.some((p) => p.requiresAuth && !isAuthPage(p));
84757
+ const hasAuthRequiredPage = siteHasAuthEvidence && targetPagesInSiteMap.some((p) => p.requiresAuth && !isAuthPage(p));
83594
84758
  if (hasAuthPage && !hasAuthRequiredPage)
83595
84759
  return ["@auth", "@discovered"];
83596
84760
  if (hasAuthRequiredPage)
@@ -83598,7 +84762,7 @@ var require_discover_generator = __commonJS({
83598
84762
  return ["@discovered", "@regression"];
83599
84763
  }
83600
84764
  function shouldReplayAuthRecipe(targetPages, siteMapPages, recipe) {
83601
- return !!recipe && computeSkeletonTags(targetPages, siteMapPages).includes("@needs-auth");
84765
+ return !!recipe && computeSkeletonTags(targetPages, siteMapPages, { extraAuthEvidence: true }).includes("@needs-auth");
83602
84766
  }
83603
84767
  function isLowConfidenceSnapshotKind(kind) {
83604
84768
  return kind === "missing" || kind === "error" || kind === "timeout" || kind === "empty" || kind === "ai-capture_fallback";
@@ -83757,6 +84921,15 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
83757
84921
  return assembled.join(separator);
83758
84922
  }
83759
84923
  var SENSITIVE_ENV_KEY_RE = /email|password|token|secret|api[_-]?key|access[_-]?key|username|user(name)?|bearer|session|cookie|\bpat\b|refresh|jwt/i;
84924
+ exports2.VALID_PLAYWRIGHT_GETBY_METHODS = /* @__PURE__ */ new Set([
84925
+ "getByRole",
84926
+ "getByText",
84927
+ "getByLabel",
84928
+ "getByPlaceholder",
84929
+ "getByTestId",
84930
+ "getByAltText",
84931
+ "getByTitle"
84932
+ ]);
83760
84933
  var VALID_PLAYWRIGHT_MATCHERS = /* @__PURE__ */ new Set([
83761
84934
  // Locator assertions
83762
84935
  "toBeAttached",
@@ -84044,6 +85217,16 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
84044
85217
  function findSensitiveLiteralLeaks(code, testVariables) {
84045
85218
  return buildSensitiveEnvReplacements(testVariables).filter((replacement) => code.includes(replacement.value)).map((replacement) => replacement.key);
84046
85219
  }
85220
+ function gotoBasePathFromBaseUrl(baseUrl) {
85221
+ if (!baseUrl)
85222
+ return void 0;
85223
+ try {
85224
+ const path = new URL(baseUrl).pathname.replace(/\/+$/, "");
85225
+ return path && path !== "/" ? path : void 0;
85226
+ } catch {
85227
+ return void 0;
85228
+ }
85229
+ }
84047
85230
  function sanitizeCode(code, options) {
84048
85231
  let result = code;
84049
85232
  const isNetworkIdleWaitLine = (line) => /^\s*await page\.waitForLoadState\((['"])networkidle\1\);\s*$/.test(line);
@@ -84066,12 +85249,24 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
84066
85249
  result = result.replace(/\s*await page\.waitForTimeout\(\d+\);\s*/g, "\n");
84067
85250
  result = result.replace(/(await page\.goto\((?:[^()()]|\([^()]*\))*\);)\s*\n\s*await expect\(page\)\.toHaveURL\((?:[^()()]|\([^()]*\))*\);/g, (_match, gotoLine) => gotoLine);
84068
85251
  result = result.replace(/const baseUrl\s*=\s*process\.env\.BASE_URL[^;]*;\s*\n/g, "");
84069
- result = result.replace(/page\.goto\(`\$\{baseUrl\}([^`]*)`/g, (_match, path) => `page.goto('${path}'`);
85252
+ result = result.replace(
85253
+ /page\.goto\(`\$\{baseUrl\}([^`]*)`/g,
85254
+ // Empty template path (goto(`${baseUrl}`)) means the app home → '/', which the
85255
+ // path-scoped basePath pass below re-joins to the real base (e.g. '/wing').
85256
+ (_match, path) => `page.goto('${path || "/"}'`
85257
+ );
84070
85258
  result = result.replace(/test\s*\(\s*(['"`].*?['"`])\s*,\s*\{\s*tags\s*:\s*\[/g, (match) => match.replace("tags", "tag"));
84071
85259
  result = result.replace(/page\.goto\(\s*['"]https?:\/\/[^/'"]+(\/[^'"]*)?['"]/g, (_match, path) => `page.goto('${path || "/"}'`);
84072
85260
  result = result.replace(/waitForURL\(\s*['"]https?:\/\/[^/'"]+(\/.+?)['"]\s*\)/g, (_match, path) => `waitForURL(/${path.replace(/\//g, "\\/")}/)`);
84073
85261
  result = result.replace(/toHaveURL\(\s*['"]https?:\/\/[^/'"]+(\/.+?)['"]\s*\)/g, (_match, path) => `toHaveURL(/${path.replace(/\//g, "\\/")}/)`);
84074
85262
  result = result.replace(/(waitForURL|toHaveURL)\(\/(.*?)\\\/\/\)/g, (_match, fn, inner) => `${fn}(/${inner}/)`);
85263
+ const basePath = options?.basePath && options.basePath !== "/" ? options.basePath.replace(/\/+$/, "") : void 0;
85264
+ if (basePath) {
85265
+ result = result.replace(/\b(goto|waitForURL|toHaveURL)\(\s*(['"])(\/(?!\/)[^'"]*)\2/g, (_match, fn, _quote, path) => {
85266
+ const joined = path === basePath || path.startsWith(basePath + "/") ? path : basePath + (path === "/" ? "" : path);
85267
+ return `${fn}('${joined}'`;
85268
+ });
85269
+ }
84075
85270
  result = result.replace(/\)\)\.(first\(\)|nth\(\d+\)|last\(\))\./g, (match, method) => `).${method}).`);
84076
85271
  result = result.replace(/^(\s+)expect\(page\b/gm, "$1await expect(page");
84077
85272
  result = result.replace(/^(\s+)page\.waitForURL\(/gm, "$1await page.waitForURL(");
@@ -84138,7 +85333,8 @@ ${testOpen}`;
84138
85333
  } : void 0;
84139
85334
  const playwrightCode = sanitizeCode(String(test.playwrightCode || ""), {
84140
85335
  sensitiveEnvMap: options?.sensitiveEnvMap,
84141
- authStamp
85336
+ authStamp,
85337
+ basePath: options?.basePath
84142
85338
  });
84143
85339
  const leakedSensitiveKeys = findSensitiveLiteralLeaks(playwrightCode, options?.sensitiveEnvMap);
84144
85340
  const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && process.env.SHARED_AUTH_INJECT === "true";
@@ -84221,6 +85417,16 @@ ${testOpen}`;
84221
85417
  return false;
84222
85418
  }
84223
85419
  }
85420
+ if ((0, discovery_lint_flags_js_1.isPwMethodPreflightEnabled)()) {
85421
+ const getByCalls = codeForMatcherScan.match(/\.getBy[A-Za-z0-9_]*\s*\(/g) ?? [];
85422
+ for (const call of getByCalls) {
85423
+ const methodName = call.replace(/[\s(]+$/, "").slice(1);
85424
+ if (!exports2.VALID_PLAYWRIGHT_GETBY_METHODS.has(methodName)) {
85425
+ logs2?.push(` \u2717 Rejected "${t.name}": invalid Playwright locator method "${methodName}" (not a real getBy* API \u2014 likely hallucinated)`);
85426
+ return false;
85427
+ }
85428
+ }
85429
+ }
84224
85430
  if (code.includes("// TODO")) {
84225
85431
  logs2?.push(` \u26A0 "${t.name}" contains TODO comment \u2014 marking as unverified`);
84226
85432
  t.tags = t.tags.filter((tag) => tag !== "@verified");
@@ -85334,6 +86540,35 @@ ${testOpen}`;
85334
86540
  }
85335
86541
  return null;
85336
86542
  }
86543
+ var STRUCTURAL_ACTION_RE = /\.(?:click|dblclick|tap|fill|type|pressSequentially|press|selectOption|hover|check|uncheck|setChecked|setInputFiles|dragTo|focus|clear)\s*\(/;
86544
+ var POSITIONAL_PSEUDO_RE = /:(?:last-child|first-child|nth-child|nth-of-type|nth-last-child|nth-last-of-type)\b/i;
86545
+ var TABLE_TOKEN_RE = /\b(?:tr|td|th|tbody|thead)\b/i;
86546
+ var ROW_ANCHOR_RE = /getByRole\s*\(\s*['"`]row['"`]|getByTestId|data-testid|data-test\b|\[role\s*=\s*['"`]row['"`]\]/i;
86547
+ var LOCATOR_ARG_RE = /\.locator\(\s*(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/g;
86548
+ function scanFragileStructuralLocators(code) {
86549
+ const findings = [];
86550
+ const scanCode = code.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
86551
+ for (const rawLine of scanCode.split("\n")) {
86552
+ const line = rawLine.replace(/\/\/.*$/, "");
86553
+ if (!STRUCTURAL_ACTION_RE.test(line))
86554
+ continue;
86555
+ if (ROW_ANCHOR_RE.test(line))
86556
+ continue;
86557
+ LOCATOR_ARG_RE.lastIndex = 0;
86558
+ let m;
86559
+ while ((m = LOCATOR_ARG_RE.exec(line)) !== null) {
86560
+ const css = m[2];
86561
+ if (POSITIONAL_PSEUDO_RE.test(css) && TABLE_TOKEN_RE.test(css)) {
86562
+ findings.push({
86563
+ line: rawLine.trim().slice(0, 200),
86564
+ reason: `Positional table-row selector "${css.slice(0, 120)}" is not scoped to a row identity \u2014 it breaks when rows reorder, sort, filter, or a row is inserted. Scope through the row: page.getByRole('row', { name: '<a stable cell value in that row>' }).getByRole('button', { name: '<verb>' }).`
86565
+ });
86566
+ break;
86567
+ }
86568
+ }
86569
+ }
86570
+ return findings;
86571
+ }
85337
86572
  function validateFinalActionFidelity(playwrightCode, actionLedger, credentials, opts) {
85338
86573
  const ledgerSigs = [];
85339
86574
  for (const entry of actionLedger) {
@@ -85540,7 +86775,11 @@ ${outcomesText}
85540
86775
  ${hasVerifiedRecipe ? "The browser is ALREADY authenticated \u2014 `__vqaLogin(page)` ran first; cookies/session are populated. Do NOT log in again; begin from the authenticated app." : "The browser starts on about:blank with NO cookies, NO session, NO localStorage. You are NOT logged in."}
85541
86776
  The base URL is: ${baseUrl}
85542
86777
  When navigating, use \`browser_navigate\` with the full URL (prepend ${baseUrl}).
85543
- In your generated test code, keep URLs RELATIVE (e.g. \`page.goto('/path')\`) \u2014 Playwright config has baseURL set.
86778
+ In your generated test code, keep URLs RELATIVE (e.g. \`page.goto('/path')\`) \u2014 Playwright config has baseURL set.${(() => {
86779
+ const basePath = gotoBasePathFromBaseUrl(baseUrl);
86780
+ return basePath ? `
86781
+ IMPORTANT: the base URL has a PATH (\`${basePath}\`), and Playwright's baseURL only prefixes relative paths \u2014 a leading-slash path escapes to the origin root. Every \`page.goto\` path MUST start with \`${basePath}\` (the app home is \`page.goto('${basePath}')\`, NOT \`page.goto('/')\`).` : "";
86782
+ })()}
85544
86783
 
85545
86784
  ${evidenceBlock}${credentialNote}
85546
86785
  ## ASSERTION VERIFICATION WORKFLOW
@@ -85748,7 +86987,9 @@ ${tailBlock}`;
85748
86987
  }
85749
86988
  }
85750
86989
  });
85751
- const tags = computeSkeletonTags(scenario.targetPages, siteMap.pages);
86990
+ const tags = computeSkeletonTags(scenario.targetPages, siteMap.pages, {
86991
+ extraAuthEvidence: !!opts?.recipe || hasConfiguredLoginCredentials(credentials)
86992
+ });
85752
86993
  const baseSkeletonHadRegression = tags.includes("@regression");
85753
86994
  const hasVerifiedRecipe = !!opts?.recipe && tags.includes("@needs-auth");
85754
86995
  const systemPrompt = buildScenarioSystemPrompt(scenario, baseUrl, credentials, tags, siteMap.pages, { appBrief: opts?.appBrief, availableVariableKeys: opts?.availableVariableKeys, hasVerifiedRecipe, knowledgeContext: opts?.knowledgeContext });
@@ -85767,6 +87008,7 @@ ${tailBlock}`;
85767
87008
  let actionRevisionAttempted = false;
85768
87009
  let ambiguityRevisionAttempted = false;
85769
87010
  let strengthRevisionAttempted = false;
87011
+ let structuralRevisionAttempted = false;
85770
87012
  let lastRenderedLedgerEntryCount = 0;
85771
87013
  const successfulVerifications = [];
85772
87014
  const actionLedger = [];
@@ -85957,7 +87199,7 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
85957
87199
  const suspectedBugDeclared = toolArgs.suspectedBug === true;
85958
87200
  const bugDescription = String(toolArgs.bugDescription || "").slice(0, 500).replace(/[\u0000-\u001F\u007F]+/g, " ").replace(/\s+/g, " ").trim();
85959
87201
  const testObj = { name, description, playwrightCode: code, tags };
85960
- const validated = validateTests([testObj], logs2, { sensitiveEnvMap: credentials, authRecipe: opts?.recipe ?? null });
87202
+ const validated = validateTests([testObj], logs2, { sensitiveEnvMap: credentials, authRecipe: opts?.recipe ?? null, basePath: gotoBasePathFromBaseUrl(baseUrl) });
85961
87203
  if (validated.length > 0) {
85962
87204
  if (actionLedger.length > lastRenderedLedgerEntryCount && actionLedger.length > 0) {
85963
87205
  const renderedLedger = renderSessionRecording(actionLedger, credentials);
@@ -86037,6 +87279,33 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
86037
87279
  });
86038
87280
  continue;
86039
87281
  }
87282
+ const structuralLintMode = (0, discovery_lint_flags_js_1.structuralLocatorLintMode)();
87283
+ const structuralFindings = structuralLintMode !== "off" ? scanFragileStructuralLocators(validated[0].playwrightCode) : [];
87284
+ if (structuralLintMode === "warn" && structuralFindings.length > 0) {
87285
+ logs2.push(` Structural-locator gate [warn]: ${structuralFindings.length} positional table-row selector(s) \u2014 ${structuralFindings.map((f) => sanitizeLedgerText(f.line)).join(" | ").slice(0, 300)}`);
87286
+ }
87287
+ if (structuralLintMode === "enforce" && structuralFindings.length > 0 && !structuralRevisionAttempted) {
87288
+ structuralRevisionAttempted = true;
87289
+ const renderedLedger = actionLedger.length > 0 ? renderSessionRecording(actionLedger, credentials) : "";
87290
+ if (renderedLedger)
87291
+ lastRenderedLedgerEntryCount = actionLedger.length;
87292
+ logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) found \u2014 requesting one revision (recording attached: ${renderedLedger ? "yes" : "no"})`);
87293
+ const structuralPayload = {
87294
+ acknowledged: false,
87295
+ valid: false,
87296
+ revisionRequired: true,
87297
+ reason: "fragile_structural_locator",
87298
+ instruction: "One or more locators below select a table row by POSITION (tr:last-child, tr:nth-child, td:nth-child) instead of by identity. Positional row selectors break the moment rows are reordered, sorted, filtered, or a row is inserted.\nFor EACH flagged locator:\n1. Scope through the row IDENTITY: page.getByRole('row', { name: '<a stable cell value in that row \u2014 an id, name, email>' }).getByRole('button', { name: '<verb>' }).\n2. If the row genuinely has no stable text, use a data-testid on the row if one exists.\n3. Keep every other line exactly as-is. Submit the COMPLETE corrected test and call finish_generate again.",
87299
+ offendingLocators: structuralFindings.map((f) => ({
87300
+ line: sanitizeLedgerText(f.line),
87301
+ reason: f.reason
87302
+ }))
87303
+ };
87304
+ if (renderedLedger)
87305
+ structuralPayload.sessionRecording = renderedLedger;
87306
+ toolResults.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(structuralPayload) });
87307
+ continue;
87308
+ }
86040
87309
  const ambiguityGateEval = evaluateAmbiguityGate(validated[0].playwrightCode, ambiguityRevisionAttempted, iteration < MAX_SCENARIO_ITERATIONS);
86041
87310
  const ambiguousLocators = ambiguityGateEval.findings;
86042
87311
  if (ambiguityGateEval.fire) {
@@ -86076,7 +87345,12 @@ Do NOT add steps you did not execute. Do NOT skip steps from the recording.${led
86076
87345
  const creditedControlSig = revealVerdict === "revealed" ? assertedSigs.find((s) => revealedControls.has(s)) ?? null : null;
86077
87346
  let strengthVerdict = null;
86078
87347
  try {
86079
- strengthVerdict = (0, shared_1.validateAssertionStrength)(validated[0].playwrightCode, { revealedControls });
87348
+ strengthVerdict = (0, shared_1.validateAssertionStrength)(validated[0].playwrightCode, {
87349
+ revealedControls,
87350
+ // Opt-in additive-contradiction gate (see assertion-strength.ts). On Fargate this
87351
+ // env must be dispatcher-forwarded to take effect here; off → byte-identical.
87352
+ flagContradictedAdditions: process.env.STRENGTH_ADDITIVE_CONTRADICTION_GATE === "true"
87353
+ });
86080
87354
  } catch (err) {
86081
87355
  logs2.push(` Strength gate errored \u2014 skipping (fail-open): ${String(err)}`);
86082
87356
  }
@@ -86128,12 +87402,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
86128
87402
  const strengthSeverity = strengthVerdict?.severity ?? "none";
86129
87403
  const hasResidualViolations = qualityCheck.offendingLines.length > 0;
86130
87404
  const hasResidualActionDrift = actionCheck.offendingActions.length > 0;
87405
+ const hasResidualStructural = structuralLintMode === "enforce" && structuralFindings.length > 0;
86131
87406
  if (hasResidualViolations) {
86132
87407
  logs2.push(` Assertion quality gate: violations remain after revision \u2014 downgrading to @needs-review`);
86133
87408
  }
86134
87409
  if (hasResidualActionDrift) {
86135
87410
  logs2.push(` Action fidelity gate: ${actionCheck.offendingActions.length} action drift(s) remain after revision \u2014 downgrading to @needs-review`);
86136
87411
  }
87412
+ if (hasResidualStructural) {
87413
+ logs2.push(` Structural-locator gate: ${structuralFindings.length} positional table-row selector(s) remain after revision \u2014 downgrading to @needs-review`);
87414
+ }
86137
87415
  let actualVerified = verified;
86138
87416
  if (verified && verifyCallCount === 0) {
86139
87417
  actualVerified = false;
@@ -86186,6 +87464,16 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
86186
87464
  residual: ambiguousLocators.length,
86187
87465
  lines: ambiguousLocators.map((a) => sanitizeLedgerText(a.line))
86188
87466
  },
87467
+ // G2 — omitted entirely when off so the off-path healingContext is
87468
+ // byte-identical; present in warn (measure) + enforce (act) modes.
87469
+ ...structuralLintMode !== "off" ? {
87470
+ structuralGate: {
87471
+ mode: structuralLintMode,
87472
+ fired: structuralRevisionAttempted,
87473
+ residual: structuralFindings.length,
87474
+ lines: structuralFindings.map((f) => sanitizeLedgerText(f.line))
87475
+ }
87476
+ } : {},
86189
87477
  // Durable marker the server promote-seam (failure-analyzer.checkTestStability)
86190
87478
  // reads to decide ACTIVE vs QUARANTINED(@bug-found) vs hold-DRAFT. Omitted when
86191
87479
  // the gate errored (fail-open) so a missing marker is treated as "promote".
@@ -86211,7 +87499,7 @@ Do NOT assert that the button/tab/heading you interacted with is merely visible
86211
87499
  verified: actualVerified,
86212
87500
  healingContext: { ...baseHealingContext, gateActivity }
86213
87501
  };
86214
- if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes) && !result.tags.includes("@needs-review")) {
87502
+ if ((hasResidualViolations || hasResidualActionDrift || hasUncoveredOutcomes || hasResidualStructural) && !result.tags.includes("@needs-review")) {
86215
87503
  result.tags.push("@needs-review");
86216
87504
  }
86217
87505
  if (actualVerified) {
@@ -301288,7 +302576,8 @@ ${currentError.slice(0, 2e3)}`
301288
302576
  }
301289
302577
  lastHealedCode = (0, discover_generator_js_1.sanitizeCode)(stripCodeFences(fixedCode), {
301290
302578
  sensitiveEnvMap: testVariables,
301291
- authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(playwrightCode, lastFailureError, logs2)
302579
+ authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(playwrightCode, lastFailureError, logs2),
302580
+ basePath: (0, discover_generator_js_1.gotoBasePathFromBaseUrl)(baseUrl)
301292
302581
  });
301293
302582
  logs2.push("\u{1F680} Running fixed code natively...");
301294
302583
  let verifyResult;
@@ -302742,7 +304031,8 @@ ${errorContent}`;
302742
304031
  }
302743
304032
  const candidateCode = (0, discover_generator_js_1.sanitizeCode)(stripCodeFences(fixedCode), {
302744
304033
  sensitiveEnvMap: testVariables,
302745
- authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(playwrightCode, options?.lastFailureError, logs2)
304034
+ authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(playwrightCode, options?.lastFailureError, logs2),
304035
+ basePath: (0, discover_generator_js_1.gotoBasePathFromBaseUrl)(baseUrl)
302746
304036
  });
302747
304037
  {
302748
304038
  const syntaxCheckCode = (0, discover_generator_js_1.stripTypescriptSyntax)((0, discover_generator_js_1.stripImportDeclarations)(candidateCode));
@@ -304489,7 +305779,8 @@ var require_grok_per_test_healer = __commonJS({
304489
305779
  }
304490
305780
  const healedCode = (0, discover_generator_js_1.sanitizeCode)(payload.playwrightCode, {
304491
305781
  sensitiveEnvMap: secrets,
304492
- authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(playwrightCode, options.lastFailureError, logs2)
305782
+ authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(playwrightCode, options.lastFailureError, logs2),
305783
+ basePath: (0, discover_generator_js_1.gotoBasePathFromBaseUrl)(baseUrl)
304493
305784
  });
304494
305785
  const leakedKey = findLeakedCredential(healedCode, secrets);
304495
305786
  if (leakedKey) {
@@ -305662,7 +306953,8 @@ var require_grok_batch_healer = __commonJS({
305662
306953
  }
305663
306954
  const healedCode = (0, discover_generator_js_1.sanitizeCode)(verdict.playwrightCode, {
305664
306955
  sensitiveEnvMap: secrets,
305665
- authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(test.playwrightCode, test.lastFailureError, logs2)
306956
+ authStamp: (0, auth_recipe_js_1.buildHealAuthStamp)(test.playwrightCode, test.lastFailureError, logs2),
306957
+ basePath: (0, discover_generator_js_1.gotoBasePathFromBaseUrl)(baseUrl)
305666
306958
  });
305667
306959
  const leakedKey = findLeakedCredential(healedCode, secrets);
305668
306960
  if (leakedKey) {
@@ -310674,7 +311966,7 @@ var require_discover_explorer = __commonJS({
310674
311966
  },
310675
311967
  requiresAuth: {
310676
311968
  type: "boolean",
310677
- description: "Whether this page requires authentication"
311969
+ description: "OBSERVED evidence only: true if reaching this page logged-out redirected to a login page, returned 401/403, or showed an explicit sign-in wall. Never infer from content looking account-ish \u2014 a page you read without logging in does NOT require auth."
310678
311970
  },
310679
311971
  observations: {
310680
311972
  type: "array",
@@ -310903,7 +312195,7 @@ You do NOT have a \`record_journey\` tool - your job is mapping, not testing. Do
310903
312195
  2. Action buttons: submit, save, delete, create, edit - include label, triggersSubmit
310904
312196
  3. Navigation links to OTHER pages within the app - include label, triggersNavigation
310905
312197
  4. Skip repeated navigation chrome you already cataloged (sidebar/header links appear on every page)
310906
- - **requiresAuth**: whether the page needs login
312198
+ - **requiresAuth**: OBSERVED evidence only \u2014 you were redirected to a login page, got 401/403, or hit a sign-in wall. If you are reading the page without having logged in, it does NOT require auth (account-ish CONTENT is not evidence)
310907
312199
  - **observations**: empty array (you are mapping, not interacting)
310908
312200
 
310909
312201
  ## Loading and readiness
@@ -314601,9 +315893,13 @@ var require_agentic_survey_prompts = __commonJS({
314601
315893
  ' route: string; // path-only, e.g. "/dashboard/profile"',
314602
315894
  ' type: string; // "AUTH" | "DASHBOARD" | "FORM" | "LIST" | ...',
314603
315895
  " summary: string; // 1-line of key elements/affordances",
314604
- " requiresAuth?: boolean; // true if reaching this page logged-out",
314605
- " // redirected to /login OR content was",
314606
- " // visibly gated behind authentication",
315896
+ " requiresAuth?: boolean; // OBSERVED evidence ONLY: you were redirected",
315897
+ " // to a login page, got 401/403, or hit an",
315898
+ " // explicit sign-in wall. NEVER infer it from",
315899
+ " // content merely LOOKING account-ish \u2014 if you",
315900
+ " // read the page without logging in, it does",
315901
+ " // NOT require auth. A site with no login at",
315902
+ " // all has requiresAuth=false everywhere.",
314607
315903
  " }>;",
314608
315904
  ' flows?: string[]; // ["/login \u2192 /dashboard (submit form)"]',
314609
315905
  " observations?: string[]; // things YOU noticed in the loop",
@@ -320437,675 +321733,6 @@ ${(0, inbox_tools_js_1.buildInboxAgentsGuidance)(inboxReg.email, inboxReg.passwo
320437
321733
  }
320438
321734
  });
320439
321735
 
320440
- // ../runner-core/dist/services/mobile/mobile-exploration-state.js
320441
- var require_mobile_exploration_state = __commonJS({
320442
- "../runner-core/dist/services/mobile/mobile-exploration-state.js"(exports2) {
320443
- "use strict";
320444
- Object.defineProperty(exports2, "__esModule", { value: true });
320445
- exports2.MobileExplorationState = void 0;
320446
- exports2.stableMobileControlIdentity = stableMobileControlIdentity;
320447
- exports2.classifySafeMobileNavigationControl = classifySafeMobileNavigationControl;
320448
- exports2.deriveScreenId = deriveScreenId;
320449
- var mobile_observation_js_1 = require_mobile_observation();
320450
- var DEFAULT_STALE_THRESHOLD = 4;
320451
- var MAX_SCREEN_OBSERVATIONS = 50;
320452
- var EDITABLE_CONTROL_ROLE_RE = /(?:android\.widget\.(?:EditText|AutoCompleteTextView)|XCUIElementType(?:TextField|SecureTextField|SearchField|TextView)|textbox|searchbox|combobox|textarea|select)$/i;
320453
- var STATEFUL_CONTROL_ROLE_RE = /(?:Switch|CheckBox|Checkbox|RadioButton|radio|Slider|SeekBar|Stepper|PickerWheel|Toggle)$/i;
320454
- var UNSAFE_OR_COMMIT_LABEL_RE = /\b(?:submit|save|confirm|delete|remove|archive|reset|disconnect|deactivate|destroy|purge|pay|purchase|buy|checkout|place\s+order|send|post|publish|upload|import|export|sign\s*out|log\s*out|logout|accept|agree|subscribe|unsubscribe|follow|unfollow|enable|disable|book|reserve|cancel|continue|next|previous|done|apply|retry|close|dismiss|sign\s*in|log\s*in|register)\b/i;
320455
- var SAFE_MODAL_TRIGGER_LABEL_RE = /^(?:create|new|add|edit|invite|configure|manage|details|preview|share)$/i;
320456
- var MENU_TRIGGER_LABEL_RE = /^(?:more|menu|options|actions|filter|filters|sort|columns|customize)$/i;
320457
- var GENERIC_NAVIGATION_DESTINATION_LABEL_RE = /^(?:home|feed|dashboard|profile|settings|inbox|messages?|notifications?|projects?|about|privacy|terms|learn\s+more)$/i;
320458
- var SYSTEM_BACK_CONTROL_LABEL_RE = /^(?:back|go\s+back|navigate\s+up|navigation\s+up|up)$/i;
320459
- var SYSTEM_BACK_CONTROL_ID_RE = /^(?:action_bar_up|arrow_back|back_arrow|navigate_up|navigation_up|nav_up|toolbar_back|action_back|btn_back|button_back|back_button|back_btn|back_icon|icon_back)$/i;
320460
- function normalizeControlIdentityPart(value) {
320461
- return value.trim().replace(/\s+/g, " ").toLowerCase();
320462
- }
320463
- function semanticControlAlias(label) {
320464
- return `navigation-label:${normalizeControlIdentityPart(label)}`;
320465
- }
320466
- function localControlCoverageIdentity(screenId, identity) {
320467
- return `screen:${screenId}:${identity}`;
320468
- }
320469
- function storedMobileControlIdentity(candidate) {
320470
- return [
320471
- candidate.identity,
320472
- `kind:${candidate.kind}`,
320473
- `label:${normalizeControlIdentityPart(candidate.label)}`
320474
- ].join(":");
320475
- }
320476
- function isSystemBackNavigationControl(element, label) {
320477
- if (SYSTEM_BACK_CONTROL_LABEL_RE.test(normalizeControlIdentityPart(label)))
320478
- return true;
320479
- return [element.accessibilityId, element.resourceId].filter((value) => Boolean(value?.trim())).some((value) => {
320480
- const leaf = normalizeControlIdentityPart(value).split(/[/:.]/).filter(Boolean).at(-1) ?? "";
320481
- return SYSTEM_BACK_CONTROL_ID_RE.test(leaf);
320482
- });
320483
- }
320484
- function stableMobileControlIdentity(element) {
320485
- const role = normalizeControlIdentityPart(element.role || "control");
320486
- const accessibilityId = element.accessibilityId?.trim();
320487
- if (accessibilityId)
320488
- return `a11y:${role}:${normalizeControlIdentityPart(accessibilityId)}`;
320489
- const resourceId = element.resourceId?.trim();
320490
- if (resourceId)
320491
- return `resource:${role}:${normalizeControlIdentityPart(resourceId)}`;
320492
- const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
320493
- if (label)
320494
- return `semantic:${role}:${normalizeControlIdentityPart(label)}`;
320495
- return null;
320496
- }
320497
- function classifySafeMobileNavigationControl(element) {
320498
- if (!element.actionable || element.enabled === false)
320499
- return null;
320500
- const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
320501
- const safetyIdentity = [label, element.accessibilityId, element.resourceId].filter(Boolean).join(" ");
320502
- if (!label || UNSAFE_OR_COMMIT_LABEL_RE.test(safetyIdentity))
320503
- return null;
320504
- if (isSystemBackNavigationControl(element, label))
320505
- return null;
320506
- const role = element.role.trim();
320507
- if (EDITABLE_CONTROL_ROLE_RE.test(role) || STATEFUL_CONTROL_ROLE_RE.test(role))
320508
- return null;
320509
- const linkLike = /(?:Link$|^a$)/i.test(role);
320510
- let kind = null;
320511
- if (/(?:TabBarItem|\btab\b)/i.test(role))
320512
- kind = "tab";
320513
- else if (/menuitem/i.test(role))
320514
- kind = "menu-item";
320515
- else if (linkLike && element.navigationProvenance)
320516
- kind = "link";
320517
- else if (linkLike)
320518
- return null;
320519
- else if (element.hasPopup && element.hasPopup.toLowerCase() !== "false")
320520
- kind = "menu-trigger";
320521
- else if (element.expanded === false)
320522
- kind = "disclosure";
320523
- else if (MENU_TRIGGER_LABEL_RE.test(label))
320524
- kind = "menu-trigger";
320525
- else if (SAFE_MODAL_TRIGGER_LABEL_RE.test(label))
320526
- kind = "modal-trigger";
320527
- else if (element.navigationProvenance)
320528
- kind = "navigation-button";
320529
- else if (GENERIC_NAVIGATION_DESTINATION_LABEL_RE.test(label))
320530
- kind = "navigation-button";
320531
- if (!kind)
320532
- return null;
320533
- const identity = stableMobileControlIdentity(element);
320534
- return identity ? { identity, label, kind } : null;
320535
- }
320536
- function sharedMobileControlCoverageIdentity(element, candidate, semanticFamilyId) {
320537
- if (!element.navigationProvenance)
320538
- return void 0;
320539
- const durableId = element.accessibilityId?.trim() ? `a11y:${normalizeControlIdentityPart(element.accessibilityId)}` : element.resourceId?.trim() ? `resource:${normalizeControlIdentityPart(element.resourceId)}` : "";
320540
- if (!durableId && !semanticFamilyId)
320541
- return void 0;
320542
- return [
320543
- "shared-navigation",
320544
- element.navigationProvenance,
320545
- !durableId && semanticFamilyId ? `family:${semanticFamilyId}` : "",
320546
- candidate.kind,
320547
- durableId || "semantic-only",
320548
- normalizeControlIdentityPart(candidate.label)
320549
- ].filter(Boolean).join(":");
320550
- }
320551
- function mobileNavigationRegion(element, windowSize) {
320552
- if (!windowSize || windowSize.width <= 0 || windowSize.height <= 0)
320553
- return "unknown";
320554
- const centerX = (element.bounds.x + element.bounds.width / 2) / windowSize.width;
320555
- const centerY = (element.bounds.y + element.bounds.height / 2) / windowSize.height;
320556
- if (centerY >= 0.72)
320557
- return "bottom";
320558
- if (centerY <= 0.28)
320559
- return "top";
320560
- if (centerX <= 0.35)
320561
- return "left";
320562
- if (centerX >= 0.65)
320563
- return "right";
320564
- return "center";
320565
- }
320566
- function buildSemanticNavigationGroups(elements, windowSize) {
320567
- const groups = /* @__PURE__ */ new Map();
320568
- for (const element of elements) {
320569
- if (!element.navigationProvenance)
320570
- continue;
320571
- const candidate = classifySafeMobileNavigationControl(element);
320572
- if (!candidate)
320573
- continue;
320574
- const groupKey = `${element.navigationProvenance}:${mobileNavigationRegion(element, windowSize)}`;
320575
- const labels = groups.get(groupKey) ?? /* @__PURE__ */ new Set();
320576
- labels.add(`${candidate.kind}:${normalizeControlIdentityPart(candidate.label)}`);
320577
- groups.set(groupKey, labels);
320578
- }
320579
- return new Map([...groups].filter(([, labels]) => labels.size >= 2));
320580
- }
320581
- function deriveScreenId(signal) {
320582
- const namespace = signal.activity?.trim() || signal.bundleId?.trim();
320583
- return namespace ? `${namespace}#${signal.screenHash}` : signal.screenHash;
320584
- }
320585
- function observedElementIdentity(element) {
320586
- return (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(element);
320587
- }
320588
- function mergeObservedElements(previous, current) {
320589
- const merged = [...current];
320590
- const seen = new Set(current.map(observedElementIdentity));
320591
- for (const element of previous) {
320592
- const identity = observedElementIdentity(element);
320593
- if (seen.has(identity))
320594
- continue;
320595
- seen.add(identity);
320596
- merged.push(element);
320597
- }
320598
- return merged;
320599
- }
320600
- function sanitizeElementsForEvidence(elements) {
320601
- return elements.map((element) => {
320602
- const label = (0, mobile_observation_js_1.safeMobileElementLabel)(element);
320603
- return {
320604
- ...element,
320605
- ...label ? { label } : { label: void 0 },
320606
- text: void 0
320607
- };
320608
- });
320609
- }
320610
- var MobileExplorationState = class {
320611
- /** screenId -> ScreenState (the mobile analog of routeState). */
320612
- screens = /* @__PURE__ */ new Map();
320613
- /** Observed directed transitions between screens. */
320614
- transitions = [];
320615
- /** Transition-announced screens that do not yet have a live observation. */
320616
- frontier = /* @__PURE__ */ new Map();
320617
- /** Safe navigation controls observed on each screen, keyed by stable identity. */
320618
- safeControlsByScreen = /* @__PURE__ */ new Map();
320619
- /** Screen-scoped coverage keys plus explicitly proven shared-navigation keys. */
320620
- attemptedSafeControlCoverageIdentities = /* @__PURE__ */ new Set();
320621
- successfulSafeControlAttemptCount = 0;
320622
- /** Repeated id-less structural navigation families, grouped by region. */
320623
- semanticNavigationFamilies = /* @__PURE__ */ new Map();
320624
- nextSemanticNavigationFamilyId = 1;
320625
- /** The screen-id of the most recently observed screen. */
320626
- currentScreenId = null;
320627
- /** Latest iteration recorded (drives stale detection + firstSeen stamps). */
320628
- iteration = 0;
320629
- /** Stale detector: progress mark (#screens + #total elements) at last advance. */
320630
- lastProgressMark = 0;
320631
- /** Consecutive iterations observed with no progress. */
320632
- staleIterations = 0;
320633
- staleThreshold;
320634
- constructor(options = {}) {
320635
- this.staleThreshold = typeof options.staleThreshold === "number" && options.staleThreshold > 0 ? Math.floor(options.staleThreshold) : DEFAULT_STALE_THRESHOLD;
320636
- }
320637
- // -------------------------------------------------------------------------
320638
- // Observation recording
320639
- // -------------------------------------------------------------------------
320640
- /**
320641
- * Returns true when `signal` identifies a screen this run has never observed
320642
- * before. Pure read — does not mutate state.
320643
- */
320644
- isNewScreen(signal) {
320645
- return !this.screens.has(deriveScreenId(signal));
320646
- }
320647
- /**
320648
- * Record an observation of `screen` at `iteration`. Creates a `ScreenState`
320649
- * on first sight (stamping `firstSeenIteration`), otherwise refreshes the
320650
- * stored element set (latest-wins, mirroring the web reducer's PAGE_CAPTURED
320651
- * element refresh). A driver observation already contains the structural UI
320652
- * tree, elements, screenshot association, and screen identity, so it is
320653
- * captured evidence immediately; the model's capture_screen call only adds
320654
- * a friendly name/type. Returns the resolved screen-id.
320655
- */
320656
- recordObservation(signal, screen, iteration) {
320657
- this.iteration = iteration;
320658
- const screenId = deriveScreenId(signal);
320659
- const evidenceElements = sanitizeElementsForEvidence(screen.elements);
320660
- const existing = this.screens.get(screenId);
320661
- if (existing) {
320662
- this.screens.set(screenId, {
320663
- ...existing,
320664
- elements: mergeObservedElements(existing.elements, evidenceElements)
320665
- });
320666
- } else {
320667
- this.screens.set(screenId, {
320668
- screenId,
320669
- firstSeenIteration: iteration,
320670
- elements: evidenceElements,
320671
- visited: true
320672
- });
320673
- }
320674
- this.frontier.delete(screenId);
320675
- const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
320676
- const semanticNavigationGroups = buildSemanticNavigationGroups(evidenceElements, screen.windowSize);
320677
- const semanticNavigationFamilyIds = /* @__PURE__ */ new Map();
320678
- for (const [groupKey, labels] of semanticNavigationGroups) {
320679
- semanticNavigationFamilyIds.set(groupKey, this.resolveSemanticNavigationFamilyId(groupKey, labels));
320680
- }
320681
- for (const element of evidenceElements) {
320682
- const candidate = classifySafeMobileNavigationControl(element);
320683
- if (!candidate)
320684
- continue;
320685
- const storedIdentity = storedMobileControlIdentity(candidate);
320686
- const semanticGroupKey = element.navigationProvenance ? `${element.navigationProvenance}:${mobileNavigationRegion(element, screen.windowSize)}` : "";
320687
- controls.set(storedIdentity, {
320688
- ...candidate,
320689
- identity: storedIdentity,
320690
- sharedCoverageIdentity: sharedMobileControlCoverageIdentity(element, candidate, semanticNavigationFamilyIds.get(semanticGroupKey))
320691
- });
320692
- }
320693
- if (controls.size > 0)
320694
- this.safeControlsByScreen.set(screenId, controls);
320695
- this.currentScreenId = screenId;
320696
- this.updateStaleProgress();
320697
- return screenId;
320698
- }
320699
- /**
320700
- * Back-compatible acknowledgement from capture_screen. Observation already
320701
- * marks structural evidence visited; this call only resolves a rare pending
320702
- * transition frontier and preserves callers that annotate explicitly.
320703
- */
320704
- markVisited(screenId) {
320705
- const state2 = this.screens.get(screenId);
320706
- if (state2 && !state2.visited) {
320707
- this.screens.set(screenId, { ...state2, visited: true });
320708
- }
320709
- this.frontier.delete(screenId);
320710
- }
320711
- /**
320712
- * Record a confirmed tap against a safe navigation candidate. Callers invoke
320713
- * this only after the driver reports success; recovery-unknown, stale-ref, and
320714
- * failed actions must remain on the breadth frontier.
320715
- */
320716
- noteControlAttempt(screenId, element) {
320717
- if (!screenId)
320718
- return false;
320719
- const candidate = classifySafeMobileNavigationControl(element);
320720
- const controls = this.safeControlsByScreen.get(screenId) ?? /* @__PURE__ */ new Map();
320721
- if (candidate) {
320722
- const storedIdentity = storedMobileControlIdentity(candidate);
320723
- const existing = controls.get(storedIdentity);
320724
- controls.set(storedIdentity, {
320725
- ...candidate,
320726
- identity: storedIdentity,
320727
- sharedCoverageIdentity: existing?.sharedCoverageIdentity ?? sharedMobileControlCoverageIdentity(element, candidate)
320728
- });
320729
- this.safeControlsByScreen.set(screenId, controls);
320730
- }
320731
- const tappedLabel = (0, mobile_observation_js_1.safeMobileElementLabel)(element)?.trim();
320732
- if (!tappedLabel)
320733
- return false;
320734
- const before = this.attemptedSafeControlCoverageIdentities.size;
320735
- const alias = semanticControlAlias(tappedLabel);
320736
- for (const control of controls.values()) {
320737
- if (semanticControlAlias(control.label) === alias) {
320738
- this.attemptedSafeControlCoverageIdentities.add(localControlCoverageIdentity(screenId, control.identity));
320739
- if (control.sharedCoverageIdentity) {
320740
- this.attemptedSafeControlCoverageIdentities.add(control.sharedCoverageIdentity);
320741
- }
320742
- }
320743
- }
320744
- if (this.attemptedSafeControlCoverageIdentities.size > before) {
320745
- this.successfulSafeControlAttemptCount += 1;
320746
- this.staleIterations = 0;
320747
- return true;
320748
- }
320749
- return false;
320750
- }
320751
- /** Attach a human-friendly name / coarse type to a screen (capture_screen). */
320752
- annotateScreen(screenId, annotation) {
320753
- const state2 = this.screens.get(screenId);
320754
- if (!state2)
320755
- return;
320756
- this.screens.set(screenId, {
320757
- ...state2,
320758
- screenName: annotation.screenName ?? state2.screenName,
320759
- screenType: annotation.screenType ?? state2.screenType
320760
- });
320761
- }
320762
- /** Attach structurally observed authentication provenance to a screen. */
320763
- setScreenAuthEvidence(screenId, evidence) {
320764
- const state2 = this.screens.get(screenId);
320765
- if (!state2)
320766
- return;
320767
- this.screens.set(screenId, {
320768
- ...state2,
320769
- requiresAuth: evidence.requiresAuth,
320770
- authProvenance: evidence.authProvenance,
320771
- sessionState: { ...evidence.sessionState }
320772
- });
320773
- }
320774
- /** Append a free-form observation note to a screen (observations tool). */
320775
- addScreenObservation(screenId, note) {
320776
- const state2 = this.screens.get(screenId);
320777
- if (!state2)
320778
- return;
320779
- const normalized = note.trim();
320780
- if (!normalized)
320781
- return;
320782
- const current = state2.observations ?? [];
320783
- if (current.some((existing) => existing.trim().toLowerCase() === normalized.toLowerCase())) {
320784
- return;
320785
- }
320786
- const observations = [...current, normalized].slice(-MAX_SCREEN_OBSERVATIONS);
320787
- this.screens.set(screenId, { ...state2, observations });
320788
- }
320789
- /**
320790
- * Record a directed transition between two screens. Both endpoints are
320791
- * ensured to exist on the frontier if not yet observed (mirrors the web
320792
- * reducer adding both ends of a TRANSITION_RECORDED to the frontier). The
320793
- * `toScreenId` becomes the current screen when the transition starts from the
320794
- * current screen.
320795
- */
320796
- recordTransition(transition) {
320797
- let recordedTransition = transition;
320798
- if (!transition.viaControlIdentity && transition.viaRef) {
320799
- const sourceElement = this.screens.get(transition.fromScreenId)?.elements.find((element) => element.ref === transition.viaRef);
320800
- if (sourceElement) {
320801
- recordedTransition = {
320802
- ...transition,
320803
- viaControlIdentity: (0, mobile_observation_js_1.stableParsedMobileElementIdentity)(sourceElement)
320804
- };
320805
- }
320806
- }
320807
- const alreadyRecorded = this.transitions.some((existing) => existing.fromScreenId === recordedTransition.fromScreenId && existing.toScreenId === recordedTransition.toScreenId && existing.action === recordedTransition.action);
320808
- if (!alreadyRecorded) {
320809
- this.transitions.push(recordedTransition);
320810
- }
320811
- for (const endpointId of [transition.fromScreenId, transition.toScreenId]) {
320812
- if (!this.screens.has(endpointId) && !this.frontier.has(endpointId)) {
320813
- this.frontier.set(endpointId, {
320814
- screenId: endpointId,
320815
- source: "transition",
320816
- discoveredAtIteration: this.iteration
320817
- });
320818
- }
320819
- }
320820
- if (transition.fromScreenId === this.currentScreenId) {
320821
- this.currentScreenId = transition.toScreenId;
320822
- }
320823
- }
320824
- // -------------------------------------------------------------------------
320825
- // Selectors
320826
- // -------------------------------------------------------------------------
320827
- /** The current screen-id (most recently observed), or null before any observation. */
320828
- getCurrentScreenId() {
320829
- return this.currentScreenId;
320830
- }
320831
- /** ScreenState for `screenId`, if known. */
320832
- getScreen(screenId) {
320833
- return this.screens.get(screenId);
320834
- }
320835
- /** Number of distinct screens observed this run. */
320836
- getScreenCount() {
320837
- return this.screens.size;
320838
- }
320839
- /** Number of screens backed by a live structural driver observation. */
320840
- getVisitedScreenCount() {
320841
- let total = 0;
320842
- for (const state2 of this.screens.values()) {
320843
- if (state2.visited)
320844
- total += 1;
320845
- }
320846
- return total;
320847
- }
320848
- /** Total elements observed across all known screens (latest snapshot each). */
320849
- getTotalElementCount() {
320850
- let total = 0;
320851
- for (const state2 of this.screens.values()) {
320852
- total += state2.elements.length;
320853
- }
320854
- return total;
320855
- }
320856
- /**
320857
- * Frontier screen-ids ranked by recency (most recently discovered first),
320858
- * excluding any screen already visited. Mirrors the web `getRankedFrontier`
320859
- * intent (filter visited, rank by recency) — without URL prefix similarity,
320860
- * which has no mobile analog.
320861
- */
320862
- getFrontier() {
320863
- return [...this.frontier.values()].filter((item) => {
320864
- const state2 = this.screens.get(item.screenId);
320865
- return !state2 || !state2.visited;
320866
- }).sort((a, b) => b.discoveredAtIteration - a.discoveredAtIteration).map((item) => item.screenId);
320867
- }
320868
- /** Safe navigation candidates not yet attempted, deduped across shared chrome. */
320869
- getUnexploredSafeNavigationCandidates() {
320870
- const authenticatedProductObserved = [...this.screens.values()].some((screen) => screen.requiresAuth === true);
320871
- const candidates = [];
320872
- for (const [screenId, controls] of this.safeControlsByScreen) {
320873
- const origin = this.screens.get(screenId);
320874
- const originIsConfirmedPublicEntry = origin?.authProvenance === "public" || !origin?.authProvenance && origin?.requiresAuth !== true && /^(?:AUTH|LANDING)$/i.test(origin?.screenType ?? "");
320875
- if (authenticatedProductObserved && originIsConfirmedPublicEntry)
320876
- continue;
320877
- for (const control of controls.values()) {
320878
- const alias = semanticControlAlias(control.label);
320879
- const coverageIdentity = control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity);
320880
- if (this.attemptedSafeControlCoverageIdentities.has(coverageIdentity) || this.attemptedSafeControlCoverageIdentities.has(localControlCoverageIdentity(screenId, control.identity)))
320881
- continue;
320882
- const existing = candidates.find((candidate) => candidate.identity === coverageIdentity || candidate.alias === alias && candidate.screenIds.has(screenId));
320883
- if (existing) {
320884
- existing.screenIds.add(screenId);
320885
- } else {
320886
- candidates.push({
320887
- identity: coverageIdentity,
320888
- label: control.label,
320889
- kind: control.kind,
320890
- alias,
320891
- screenIds: /* @__PURE__ */ new Set([screenId])
320892
- });
320893
- }
320894
- }
320895
- }
320896
- return candidates.map((candidate) => ({
320897
- identity: candidate.identity,
320898
- label: candidate.label,
320899
- kind: candidate.kind,
320900
- screenIds: [...candidate.screenIds]
320901
- })).sort((a, b) => {
320902
- const aCurrent = this.currentScreenId !== null && a.screenIds.includes(this.currentScreenId);
320903
- const bCurrent = this.currentScreenId !== null && b.screenIds.includes(this.currentScreenId);
320904
- if (aCurrent !== bCurrent)
320905
- return aCurrent ? -1 : 1;
320906
- return b.screenIds.length - a.screenIds.length;
320907
- });
320908
- }
320909
- /** Harness-owned survey completion floor, independent of model assertions. */
320910
- getSurveyBreadthCoverage() {
320911
- const unvisitedScreenIds = this.getFrontier();
320912
- const unexploredControls = this.getUnexploredSafeNavigationCandidates();
320913
- const complete = unvisitedScreenIds.length === 0 && unexploredControls.length === 0;
320914
- const missing = [];
320915
- if (unvisitedScreenIds.length > 0) {
320916
- missing.push(`${unvisitedScreenIds.length} uncaptured screen(s)`);
320917
- }
320918
- if (unexploredControls.length > 0) {
320919
- missing.push(`${unexploredControls.length} safe navigation control(s)`);
320920
- }
320921
- return {
320922
- required: true,
320923
- complete,
320924
- unvisitedScreenIds,
320925
- unexploredControls,
320926
- attemptedControlCount: this.successfulSafeControlAttemptCount,
320927
- ...missing.length > 0 ? { reason: `Missing ${missing.join(" and ")}.` } : {}
320928
- };
320929
- }
320930
- /**
320931
- * Monotonic structural progress token for adaptive survey leases. Frontier
320932
- * cardinality alone is insufficient: attempting control A can reveal control
320933
- * B and leave the remaining count unchanged even though real work happened.
320934
- */
320935
- getSurveyBreadthProgressMark() {
320936
- const discoveredControlIdentities = /* @__PURE__ */ new Set();
320937
- for (const [screenId, controls] of this.safeControlsByScreen) {
320938
- for (const control of controls.values()) {
320939
- discoveredControlIdentities.add(control.sharedCoverageIdentity ?? localControlCoverageIdentity(screenId, control.identity));
320940
- }
320941
- }
320942
- return [
320943
- `screens:${this.screens.size}`,
320944
- `attempts:${this.successfulSafeControlAttemptCount}`,
320945
- `controls:${discoveredControlIdentities.size}`,
320946
- `transitions:${this.transitions.length}`
320947
- ].join("|");
320948
- }
320949
- resolveSemanticNavigationFamilyId(groupKey, labels) {
320950
- const families = this.semanticNavigationFamilies.get(groupKey) ?? [];
320951
- let best;
320952
- let bestIntersection = 0;
320953
- for (const family of families) {
320954
- let intersection = 0;
320955
- for (const label of labels) {
320956
- if (family.labels.has(label))
320957
- intersection += 1;
320958
- }
320959
- const overlap = intersection / Math.min(labels.size, family.labels.size);
320960
- if (intersection >= 2 && overlap >= 0.6 && intersection > bestIntersection) {
320961
- best = family;
320962
- bestIntersection = intersection;
320963
- }
320964
- }
320965
- if (best) {
320966
- for (const label of labels)
320967
- best.labels.add(label);
320968
- return best.id;
320969
- }
320970
- const created = {
320971
- id: `semantic-nav-family-${this.nextSemanticNavigationFamilyId++}`,
320972
- labels: new Set(labels)
320973
- };
320974
- families.push(created);
320975
- this.semanticNavigationFamilies.set(groupKey, families);
320976
- return created.id;
320977
- }
320978
- /** Observed transitions (defensive copy). */
320979
- getTransitions() {
320980
- return [...this.transitions];
320981
- }
320982
- // -------------------------------------------------------------------------
320983
- // Stale-progress detection (ported semantics from the web stale guard)
320984
- // -------------------------------------------------------------------------
320985
- /**
320986
- * Progress mark: distinct screens weighted heavily, plus total elements seen.
320987
- * A run advances this mark whenever it finds a new screen OR new elements;
320988
- * the stale detector triggers when the mark stops advancing for
320989
- * `staleThreshold` consecutive iterations.
320990
- */
320991
- getProgressMark() {
320992
- return this.screens.size * 1e3 + this.transitions.length * 100 + this.successfulSafeControlAttemptCount * 10 + this.getTotalElementCount();
320993
- }
320994
- updateStaleProgress() {
320995
- const mark = this.getProgressMark();
320996
- if (mark > this.lastProgressMark) {
320997
- this.lastProgressMark = mark;
320998
- this.staleIterations = 0;
320999
- } else {
321000
- this.staleIterations += 1;
321001
- }
321002
- }
321003
- /** Consecutive no-progress iterations observed so far. */
321004
- getStaleIterations() {
321005
- return this.staleIterations;
321006
- }
321007
- /**
321008
- * True when the run has gone `staleThreshold` consecutive iterations with no
321009
- * new screen and no new element — the signal the explorer uses to nudge or
321010
- * finish (mirrors the web explorer's stale guard).
321011
- */
321012
- isStaleProgress() {
321013
- return this.staleIterations >= this.staleThreshold;
321014
- }
321015
- // -------------------------------------------------------------------------
321016
- // Planner evidence export
321017
- // -------------------------------------------------------------------------
321018
- /**
321019
- * Snapshot the screen-graph as a plain object (frozen mobile-types
321020
- * `MobileScreenGraph`). Defensive copies so callers cannot mutate internal
321021
- * state.
321022
- */
321023
- toScreenGraph() {
321024
- const screens = /* @__PURE__ */ new Map();
321025
- for (const [id, state2] of this.screens) {
321026
- screens.set(id, {
321027
- ...state2,
321028
- elements: [...state2.elements],
321029
- observations: state2.observations ? [...state2.observations] : void 0,
321030
- sessionState: state2.sessionState ? { ...state2.sessionState } : void 0
321031
- });
321032
- }
321033
- return {
321034
- screens,
321035
- transitions: this.transitions.map((t) => ({
321036
- fromScreenId: t.fromScreenId,
321037
- toScreenId: t.toScreenId,
321038
- viaRef: t.viaRef,
321039
- viaControlIdentity: t.viaControlIdentity,
321040
- action: t.action
321041
- }))
321042
- };
321043
- }
321044
- /**
321045
- * Build the planner evidence shape: one `CollectedPageData` per screen, with
321046
- * the screen-id carried in BOTH `route` (the on-the-wire positional field the
321047
- * server keys screenshots by) AND the explicit `screenId` field (per the
321048
- * Phase-0 `CollectedPageData.screenId` contract). `elements` are mapped from
321049
- * the screen's parsed actionable elements into the platform-neutral
321050
- * `CollectedPageElement[]` shape the server persists as SiteElements — without
321051
- * this, mobile screens land with zero elements (coverage always 0%, no element
321052
- * intelligence). `triggersNavigation` is derived from the durable source
321053
- * element identity captured when the transition happened (with ref-only
321054
- * fallback for legacy transitions). Insertion order matches first-seen order.
321055
- */
321056
- buildInMemoryScreenMap() {
321057
- const pages = [];
321058
- const ordered = [...this.screens.values()].sort((a, b) => a.firstSeenIteration - b.firstSeenIteration);
321059
- const navRefsByScreen = /* @__PURE__ */ new Map();
321060
- const navIdentitiesByScreen = /* @__PURE__ */ new Map();
321061
- for (const transition of this.transitions) {
321062
- if (transition.viaControlIdentity) {
321063
- const set = navIdentitiesByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
321064
- set.add(transition.viaControlIdentity);
321065
- navIdentitiesByScreen.set(transition.fromScreenId, set);
321066
- } else if (transition.viaRef) {
321067
- const set = navRefsByScreen.get(transition.fromScreenId) ?? /* @__PURE__ */ new Set();
321068
- set.add(transition.viaRef);
321069
- navRefsByScreen.set(transition.fromScreenId, set);
321070
- }
321071
- }
321072
- for (const state2 of ordered) {
321073
- const page = {
321074
- route: state2.screenId,
321075
- screenId: state2.screenId,
321076
- pageType: state2.screenType ?? "UNKNOWN",
321077
- elements: (0, mobile_observation_js_1.buildCollectedElementsForScreen)(state2.elements, {
321078
- navigationRefs: navRefsByScreen.get(state2.screenId),
321079
- navigationIdentities: navIdentitiesByScreen.get(state2.screenId)
321080
- }),
321081
- requiresAuth: state2.requiresAuth ?? false
321082
- };
321083
- if (state2.sessionState) {
321084
- page.sessionState = { ...state2.sessionState };
321085
- }
321086
- if (state2.screenName) {
321087
- page.title = state2.screenName;
321088
- }
321089
- if (state2.observations && state2.observations.length > 0) {
321090
- page.observations = state2.observations.map((note) => ({
321091
- action: "observe",
321092
- outcome: note,
321093
- outcomeType: "SUCCESS"
321094
- }));
321095
- }
321096
- pages.push(page);
321097
- }
321098
- return pages;
321099
- }
321100
- /** Alias for `buildInMemoryScreenMap()` — name parity with the planner-evidence call site. */
321101
- toScreenMap() {
321102
- return this.buildInMemoryScreenMap();
321103
- }
321104
- };
321105
- exports2.MobileExplorationState = MobileExplorationState;
321106
- }
321107
- });
321108
-
321109
321736
  // ../runner-core/dist/services/mobile/mobile-discovery-agent.js
321110
321737
  var require_mobile_discovery_agent = __commonJS({
321111
321738
  "../runner-core/dist/services/mobile/mobile-discovery-agent.js"(exports2) {
@@ -321401,7 +322028,7 @@ Explore complete: ${explore.exploreStats.pagesDiscovered} screens, ${explore.exp
321401
322028
  };
321402
322029
  }
321403
322030
  if (isSurvey && explore.authCoverage && !explore.authCoverage.complete) {
321404
- throw new Error("Mobile survey authentication coverage is incomplete; planning was skipped. " + (explore.authCoverage.reason ?? "No authenticated product screen was proven."));
322031
+ log2("Mobile survey continuing to planning with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "No authenticated product screen was proven."));
321405
322032
  }
321406
322033
  if (isSurvey && explore.breadthCoverage && !explore.breadthCoverage.complete) {
321407
322034
  log2("Mobile survey continuing to planning with residual breadth gaps: " + (explore.breadthCoverage.reason ?? "Unvisited screens or safe navigation controls remain."));
@@ -321662,6 +322289,9 @@ Fatal error: ${msg}`);
321662
322289
  if (explore.breadthCoverage && !explore.breadthCoverage.complete) {
321663
322290
  warnings.push("Discovery retained the captured mobile screen graph with residual navigation gaps: " + (explore.breadthCoverage.reason ?? "some observed controls could not be revisited."));
321664
322291
  }
322292
+ if (explore.authCoverage && !explore.authCoverage.complete) {
322293
+ warnings.push("Discovery retained the captured mobile screen graph with incomplete authenticated coverage: " + (explore.authCoverage.reason ?? "no authenticated product screen was proven."));
322294
+ }
321665
322295
  if (explore.truncated) {
321666
322296
  const reasonLabel = explore.truncatedReason === "rate_limit" ? "AI rate-limit" : explore.truncatedReason === "timeout" ? "AI timeout" : "AI error";
321667
322297
  warnings.push(`Discovery may be incomplete: exploration aborted by ${reasonLabel}. Re-run after the limit clears for a complete screen graph.`);
@@ -336171,7 +336801,7 @@ var require_package4 = __commonJS({
336171
336801
  "package.json"(exports2, module2) {
336172
336802
  module2.exports = {
336173
336803
  name: "@validate.qa/runner",
336174
- version: "1.0.37",
336804
+ version: "1.0.39",
336175
336805
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
336176
336806
  bin: {
336177
336807
  "validate-runner": "dist/cli.js"
@@ -339426,8 +340056,9 @@ async function activateTargetApp(sessionId, platform3, appId) {
339426
340056
  const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
339427
340057
  await executeMobileScript(sessionId, "mobile: activateApp", args);
339428
340058
  }
340059
+ var APP_TERMINATE_TIMEOUT_MS = 5e3;
339429
340060
  async function terminateTargetApp(sessionId, platform3, appId) {
339430
- const args = platform3 === "IOS" ? [{ bundleId: appId }] : [{ appId }];
340061
+ const args = platform3 === "IOS" ? [{ bundleId: appId, timeout: APP_TERMINATE_TIMEOUT_MS }] : [{ appId, timeout: APP_TERMINATE_TIMEOUT_MS }];
339431
340062
  await executeMobileScript(sessionId, "mobile: terminateApp", args);
339432
340063
  }
339433
340064
  async function freshLaunchTargetApp(sessionId, platform3, appId, log2) {
@@ -342438,7 +343069,11 @@ var MobileBridgeDriver = class {
342438
343069
  async relaunch() {
342439
343070
  await this.withSessionRecovery("app relaunch", async () => {
342440
343071
  const args = this.appLifecycleArgs();
342441
- await this.executeScript("mobile: terminateApp", [args]);
343072
+ try {
343073
+ await this.executeScript("mobile: terminateApp", [{ ...args, timeout: 5e3 }]);
343074
+ } catch (err) {
343075
+ if (isRecoverableAppiumSessionFailure(err)) throw err;
343076
+ }
342442
343077
  await this.executeScript("mobile: activateApp", [args]);
342443
343078
  try {
342444
343079
  await waitForColdBootSettle(this.sessionPath);