@appilots/cli 0.11.0 → 0.11.3

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.
package/dist/cli/index.js CHANGED
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var import_commander8 = require("commander");
32
32
 
33
33
  // src/version.ts
34
- var CLI_VERSION = "0.11.0";
34
+ var CLI_VERSION = "0.11.3";
35
35
 
36
36
  // src/config/index.ts
37
37
  var import_fs = require("fs");
@@ -107,12 +107,12 @@ function getEnvOverrides(env = process.env) {
107
107
  return trimmed ? trimmed : void 0;
108
108
  };
109
109
  return {
110
- apiKey: clean(env.APPILOTS_API_KEY),
110
+ apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
111
111
  projectId: clean(env.APPILOTS_PROJECT_ID),
112
112
  serverUrl: clean(env.APPILOTS_SERVER_URL)
113
113
  };
114
114
  }
115
- function loadConfig(onWarn) {
115
+ function loadConfig(onWarn, options = {}) {
116
116
  const configPath = getConfigPath();
117
117
  const env = getEnvOverrides();
118
118
  let fileConfig = {};
@@ -151,6 +151,10 @@ function loadConfig(onWarn) {
151
151
  };
152
152
  const validation = validateConfig(merged);
153
153
  validation.warnings.unshift(...legacyWarnings);
154
+ if (options.requireApiKey === false) {
155
+ validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
156
+ validation.valid = validation.errors.length === 0;
157
+ }
154
158
  if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
155
159
  for (const key of Object.keys(fileConfig)) {
156
160
  if (KNOWN_CONFIG_KEYS.includes(key)) continue;
@@ -763,12 +767,18 @@ function initCommand() {
763
767
  // src/cli/commands/sync.ts
764
768
  var import_commander2 = require("commander");
765
769
  var import_promises5 = require("fs/promises");
766
- var import_node_path5 = require("path");
770
+ var import_node_path7 = require("path");
767
771
 
768
772
  // src/generators/MCPGenerator.ts
769
773
  var import_promises4 = require("fs/promises");
770
774
  var import_node_crypto = require("crypto");
771
- var import_node_path3 = __toESM(require("path"));
775
+ var import_node_path5 = __toESM(require("path"));
776
+
777
+ // src/generators/checksum.ts
778
+ function serializeForChecksum(document) {
779
+ const { generatedAt: _generatedAt, ...content } = document;
780
+ return JSON.stringify(content, null, 2);
781
+ }
772
782
 
773
783
  // src/pipeline/enrichment.ts
774
784
  function enrichScreenForAgent(screen) {
@@ -846,7 +856,10 @@ function mergeTargets(targets) {
846
856
  for (const target of targets) {
847
857
  if (!target.id) continue;
848
858
  const existing = byId.get(target.id);
849
- byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
859
+ byId.set(
860
+ target.id,
861
+ existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
862
+ );
850
863
  }
851
864
  return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
852
865
  }
@@ -891,7 +904,10 @@ function synthesizeFlows(screen, targets) {
891
904
  intent: "destructive_action",
892
905
  steps: [
893
906
  { type: "press", target: action.id, label: action.label },
894
- { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
907
+ {
908
+ type: "confirm",
909
+ description: "Wait for native or custom confirmation before continuing"
910
+ },
895
911
  { type: "wait", description: describeWait(action) }
896
912
  ],
897
913
  waitPolicy: waitPolicyForAction(action),
@@ -906,10 +922,19 @@ function synthesizeFlows(screen, targets) {
906
922
  title: `Act on an item in ${collection.id}`,
907
923
  intent: "list_action",
908
924
  steps: [
909
- { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
910
- { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
925
+ {
926
+ type: "choose-list-item",
927
+ target: collection.id,
928
+ description: "Resolve the user reference to a visible or searchable row"
929
+ },
930
+ {
931
+ type: "press",
932
+ description: collection.rowAction?.description ?? "Open the row action"
933
+ }
911
934
  ],
912
- waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
935
+ waitPolicy: {
936
+ expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
937
+ }
913
938
  });
914
939
  }
915
940
  }
@@ -917,25 +942,32 @@ function synthesizeFlows(screen, targets) {
917
942
  }
918
943
  function waitPolicyForAction(action) {
919
944
  const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
920
- const signals = [action.successSignal, action.failureSignal].filter(Boolean);
945
+ const signals = [action.successSignal, action.failureSignal].filter(
946
+ Boolean
947
+ );
948
+ const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
921
949
  return {
922
950
  ...expectedOutcome ? { expectedOutcome } : {},
923
951
  ...signals && signals.length > 0 ? { signals } : {},
924
- ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
952
+ ...maxMs !== void 0 ? { maxMs } : {}
925
953
  };
926
954
  }
927
955
  function describeWait(action) {
928
956
  if (action.successSignal?.description) return action.successSignal.description;
929
- if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
957
+ if (action.successSignal?.type === "goBack")
958
+ return "Wait for the app to return to the previous screen";
930
959
  if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
931
- if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
960
+ if (action.appilotsInferred?.expectedOutcome)
961
+ return `Wait for ${action.appilotsInferred.expectedOutcome}`;
932
962
  return "Wait for the UI to settle";
933
963
  }
934
964
  function synthesizeAgentHints(screen, targets, flows) {
935
965
  const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
936
966
  const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
937
967
  const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
938
- const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
968
+ const firstAsyncAction = screen.actions.find(
969
+ (action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
970
+ );
939
971
  const hints = {
940
972
  primaryGoal: synthesizePrimaryGoal(screen),
941
973
  commonTasks,
@@ -962,7 +994,9 @@ function synthesizePrimaryGoal(screen) {
962
994
  }
963
995
  const fieldCount = uniqueFields.size;
964
996
  if (fieldCount > 0) {
965
- const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
997
+ const requiredCount = Array.from(uniqueFields.values()).filter(
998
+ (field) => field.required
999
+ ).length;
966
1000
  const fieldLabel = fieldCount === 1 ? "field" : "fields";
967
1001
  goals.push(
968
1002
  requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
@@ -976,7 +1010,9 @@ function synthesizePrimaryGoal(screen) {
976
1010
  if (submitCount > 0) {
977
1011
  goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
978
1012
  }
979
- const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
1013
+ const asyncCount = screen.actions.filter(
1014
+ (action) => action.appilotsInferred?.isAsyncTrigger
1015
+ ).length;
980
1016
  if (asyncCount > 0) {
981
1017
  goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
982
1018
  }
@@ -986,15 +1022,23 @@ function synthesizePrimaryGoal(screen) {
986
1022
  }
987
1023
 
988
1024
  // src/analyzers/ReactNativePlatformAnalyzer.ts
989
- var import_node_path = __toESM(require("path"));
990
- var import_fast_glob3 = __toESM(require("fast-glob"));
1025
+ var import_node_path3 = __toESM(require("path"));
1026
+
1027
+ // src/utils/glob.ts
1028
+ var import_fast_glob = __toESM(require("fast-glob"));
1029
+ function byCodeUnit(a, b) {
1030
+ return a < b ? -1 : a > b ? 1 : 0;
1031
+ }
1032
+ async function globSorted(patterns, options) {
1033
+ const files = await (0, import_fast_glob.default)(patterns, options);
1034
+ return files.sort(byCodeUnit);
1035
+ }
991
1036
 
992
1037
  // src/analyzers/ScreenAnalyzer.ts
993
1038
  var import_promises = __toESM(require("fs/promises"));
994
1039
  var import_path3 = __toESM(require("path"));
995
1040
  var import_traverse4 = __toESM(require("@babel/traverse"));
996
1041
  var BabelTypes = __toESM(require("@babel/types"));
997
- var import_fast_glob = __toESM(require("fast-glob"));
998
1042
 
999
1043
  // src/ast/parse.ts
1000
1044
  var import_parser = require("@babel/parser");
@@ -1397,14 +1441,25 @@ var ScreenAnalyzer = class {
1397
1441
  strictScreens;
1398
1442
  /** Glob patterns that identify screen files in strict mode */
1399
1443
  screenPatterns;
1444
+ /**
1445
+ * Arquivos que uma ROTA monta (`component={…}` resolvido pelo
1446
+ * `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
1447
+ * convenção de nome, porque um componente que uma rota monta É uma tela por
1448
+ * definição — não é heurística, é o que o app declarou.
1449
+ *
1450
+ * É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
1451
+ * em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
1452
+ */
1453
+ routeTargetFiles;
1400
1454
  /** §D: Count of screens filtered out in strict mode (available after analyze()) */
1401
1455
  screensFilteredOut = 0;
1402
1456
  constructor(config, options) {
1403
1457
  this.config = config;
1458
+ this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
1404
1459
  this.strictScreens = options?.strictScreens ?? false;
1405
1460
  this.screenPatterns = options?.screenPatterns ?? [
1406
- "**/*Screen.{ts,tsx}",
1407
- "**/screens/**/*.{ts,tsx}"
1461
+ "**/*Screen.{ts,tsx,js,jsx}",
1462
+ "**/screens/**/*.{ts,tsx,js,jsx}"
1408
1463
  ];
1409
1464
  }
1410
1465
  /** Analyze all screens in the project */
@@ -1416,7 +1471,7 @@ var ScreenAnalyzer = class {
1416
1471
  console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
1417
1472
  }
1418
1473
  }
1419
- const files = await (0, import_fast_glob.default)(include, {
1474
+ const files = await globSorted(include, {
1420
1475
  cwd: this.config.rootDir,
1421
1476
  ignore: exclude
1422
1477
  });
@@ -1425,7 +1480,7 @@ var ScreenAnalyzer = class {
1425
1480
  }
1426
1481
  let screenPatternFiles = null;
1427
1482
  if (this.strictScreens) {
1428
- const matched = await (0, import_fast_glob.default)(this.screenPatterns, {
1483
+ const matched = await globSorted(this.screenPatterns, {
1429
1484
  cwd: this.config.rootDir,
1430
1485
  ignore: exclude
1431
1486
  });
@@ -1441,7 +1496,8 @@ var ScreenAnalyzer = class {
1441
1496
  if (this.strictScreens) {
1442
1497
  const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
1443
1498
  const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
1444
- if (!hasRegisterScreen && !matchesPattern) {
1499
+ const isRouteTarget = this.routeTargetFiles.has(filePath);
1500
+ if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
1445
1501
  this.screensFilteredOut++;
1446
1502
  if (this.verbose) {
1447
1503
  console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
@@ -1649,6 +1705,8 @@ var ScreenAnalyzer = class {
1649
1705
  action.riskLevel = value.value;
1650
1706
  } else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
1651
1707
  action.nativeConfirmationExpected = value.value;
1708
+ } else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
1709
+ action.asyncBudgetMs = value.value;
1652
1710
  } else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
1653
1711
  action.appilotsInferred = this.parseAppilotsInferredObject(value);
1654
1712
  }
@@ -2636,43 +2694,880 @@ var ScreenAnalyzer = class {
2636
2694
  // src/analyzers/NavigationAnalyzer.ts
2637
2695
  var import_fs4 = require("fs");
2638
2696
  var import_path4 = __toESM(require("path"));
2639
- var import_fast_glob2 = __toESM(require("fast-glob"));
2640
- var parser = __toESM(require("@babel/parser"));
2697
+ var parser2 = __toESM(require("@babel/parser"));
2641
2698
  var import_traverse5 = __toESM(require("@babel/traverse"));
2699
+ var t6 = __toESM(require("@babel/types"));
2700
+
2701
+ // src/ast/navigation/module-graph.ts
2702
+ var import_node_fs = require("fs");
2703
+ var import_node_path = __toESM(require("path"));
2704
+ var parser = __toESM(require("@babel/parser"));
2642
2705
  var t5 = __toESM(require("@babel/types"));
2706
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
2707
+ var MAX_HOPS = 8;
2708
+ var ModuleGraph = class {
2709
+ asts = /* @__PURE__ */ new Map();
2710
+ resolved = /* @__PURE__ */ new Map();
2711
+ /** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
2712
+ aliases;
2713
+ /** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
2714
+ workspacePackages;
2715
+ constructor(rootDir) {
2716
+ this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
2717
+ this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
2718
+ }
2719
+ /** AST de um arquivo, memoizada. `null` quando não parseia. */
2720
+ parse(file2) {
2721
+ const cached = this.asts.get(file2);
2722
+ if (cached !== void 0) return cached;
2723
+ let ast = null;
2724
+ try {
2725
+ ast = parser.parse((0, import_node_fs.readFileSync)(file2, "utf-8"), {
2726
+ sourceType: "module",
2727
+ plugins: ["jsx", "typescript"]
2728
+ });
2729
+ } catch {
2730
+ ast = null;
2731
+ }
2732
+ this.asts.set(file2, ast);
2733
+ return ast;
2734
+ }
2735
+ /**
2736
+ * `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
2737
+ * Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
2738
+ * é de terceiro e não tem tela nossa dentro.
2739
+ */
2740
+ resolve(fromFile, spec) {
2741
+ const key = `${fromFile} ${spec}`;
2742
+ const cached = this.resolved.get(key);
2743
+ if (cached !== void 0) return cached;
2744
+ let base = null;
2745
+ if (spec.startsWith(".")) {
2746
+ base = import_node_path.default.resolve(import_node_path.default.dirname(fromFile), spec);
2747
+ } else {
2748
+ for (const { prefix, target } of this.aliases) {
2749
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
2750
+ base = import_node_path.default.join(target, spec.slice(prefix.length));
2751
+ break;
2752
+ }
2753
+ }
2754
+ if (!base) {
2755
+ for (const pkg of this.workspacePackages) {
2756
+ if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
2757
+ base = import_node_path.default.join(pkg.dir, spec.slice(pkg.name.length));
2758
+ break;
2759
+ }
2760
+ }
2761
+ }
2762
+ }
2763
+ if (!base) {
2764
+ this.resolved.set(key, null);
2765
+ return null;
2766
+ }
2767
+ const candidates = [
2768
+ base,
2769
+ ...EXTENSIONS.map((e) => base + e),
2770
+ ...EXTENSIONS.map((e) => import_node_path.default.join(base, "index" + e))
2771
+ ];
2772
+ let found = null;
2773
+ for (const c of candidates) {
2774
+ try {
2775
+ if ((0, import_node_fs.existsSync)(c) && (0, import_node_fs.statSync)(c).isFile()) {
2776
+ found = c;
2777
+ break;
2778
+ }
2779
+ } catch {
2780
+ }
2781
+ }
2782
+ this.resolved.set(key, found);
2783
+ return found;
2784
+ }
2785
+ /** `import`s do arquivo, por nome local. */
2786
+ imports(file2) {
2787
+ const out = /* @__PURE__ */ new Map();
2788
+ const ast = this.parse(file2);
2789
+ if (!ast) return out;
2790
+ for (const stmt of ast.program.body) {
2791
+ if (!t5.isImportDeclaration(stmt)) continue;
2792
+ const source = stmt.source.value;
2793
+ for (const spec of stmt.specifiers) {
2794
+ if (t5.isImportDefaultSpecifier(spec)) {
2795
+ out.set(spec.local.name, { source, imported: "default" });
2796
+ } else if (t5.isImportNamespaceSpecifier(spec)) {
2797
+ out.set(spec.local.name, { source, imported: "*" });
2798
+ } else if (t5.isImportSpecifier(spec)) {
2799
+ const imported = t5.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
2800
+ out.set(spec.local.name, { source, imported });
2801
+ }
2802
+ }
2803
+ }
2804
+ return out;
2805
+ }
2806
+ /** `const X = <init>` no topo do arquivo, incluindo `export const`. */
2807
+ topLevelInit(file2, name) {
2808
+ const ast = this.parse(file2);
2809
+ if (!ast) return null;
2810
+ for (const stmt of ast.program.body) {
2811
+ const decl = t5.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
2812
+ if (!t5.isVariableDeclaration(decl)) continue;
2813
+ for (const d of decl.declarations) {
2814
+ if (t5.isIdentifier(d.id) && d.id.name === name && d.init) {
2815
+ return t5.isTSAsExpression(d.init) ? d.init.expression : d.init;
2816
+ }
2817
+ }
2818
+ }
2819
+ return null;
2820
+ }
2821
+ /**
2822
+ * Onde `name` é DEFINIDO — segue import e reexport de barrel.
2823
+ * Devolve o arquivo e o nome sob o qual ele é definido lá.
2824
+ */
2825
+ resolveBinding(file2, name, hops = 0) {
2826
+ if (hops > MAX_HOPS) return null;
2827
+ if (this.topLevelInit(file2, name) !== null) return { file: file2, name };
2828
+ const binding = this.imports(file2).get(name);
2829
+ if (binding) {
2830
+ const target = this.resolve(file2, binding.source);
2831
+ if (!target) return null;
2832
+ const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
2833
+ const deeper = this.resolveBinding(target, next, hops + 1);
2834
+ return deeper ?? { file: target, name: next };
2835
+ }
2836
+ const ast = this.parse(file2);
2837
+ if (ast) {
2838
+ for (const stmt of ast.program.body) {
2839
+ if (!t5.isExportNamedDeclaration(stmt) || !stmt.source) continue;
2840
+ for (const spec of stmt.specifiers) {
2841
+ if (!t5.isExportSpecifier(spec)) continue;
2842
+ const exported = t5.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
2843
+ if (exported !== name) continue;
2844
+ const target = this.resolve(file2, stmt.source.value);
2845
+ if (!target) return null;
2846
+ const local = spec.local.name;
2847
+ return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
2848
+ }
2849
+ }
2850
+ }
2851
+ if (ast) {
2852
+ for (const stmt of ast.program.body) {
2853
+ if (!t5.isExportAllDeclaration(stmt)) continue;
2854
+ const target = this.resolve(file2, stmt.source.value);
2855
+ if (!target || target === file2) continue;
2856
+ const deeper = this.resolveBinding(target, name, hops + 1);
2857
+ if (deeper) return deeper;
2858
+ }
2859
+ }
2860
+ if (ast) {
2861
+ for (const stmt of ast.program.body) {
2862
+ if (!t5.isExportDefaultDeclaration(stmt)) continue;
2863
+ if (t5.isIdentifier(stmt.declaration)) {
2864
+ const local = stmt.declaration.name;
2865
+ if (local === name) return null;
2866
+ return this.resolveBinding(file2, local, hops + 1) ?? { file: file2, name: local };
2867
+ }
2868
+ }
2869
+ }
2870
+ return null;
2871
+ }
2872
+ /**
2873
+ * O valor string de uma expressão de nome de rota, ou `null`.
2874
+ *
2875
+ * Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
2876
+ * seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
2877
+ * com interpolação nem valor calculado, de propósito.
2878
+ */
2879
+ stringConstant(file2, node, hops = 0) {
2880
+ if (!node || hops > MAX_HOPS) return null;
2881
+ if (t5.isStringLiteral(node)) return node.value;
2882
+ if (t5.isTemplateLiteral(node)) {
2883
+ return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
2884
+ }
2885
+ if (t5.isTSAsExpression(node)) return this.stringConstant(file2, node.expression, hops + 1);
2886
+ const chain = memberChain(node);
2887
+ if (!chain) return null;
2888
+ const origin = this.resolveBinding(file2, chain.root);
2889
+ if (!origin) return null;
2890
+ let current = this.topLevelInit(origin.file, origin.name);
2891
+ if (!current) return null;
2892
+ for (const key of chain.path) {
2893
+ if (!t5.isObjectExpression(current)) return null;
2894
+ const prop = objectProperty(current, key);
2895
+ if (!prop) return null;
2896
+ current = t5.isTSAsExpression(prop) ? prop.expression : prop;
2897
+ }
2898
+ return t5.isStringLiteral(current) ? current.value : null;
2899
+ }
2900
+ /**
2901
+ * O ARQUIVO onde vive o componente de uma rota, ou `null`.
2902
+ *
2903
+ * Aceita as três formas que o corpus mostrou: identificador
2904
+ * (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
2905
+ * e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
2906
+ * que é como o `pocketpal` monta todas as telas do Drawer).
2907
+ */
2908
+ componentFile(file2, node, hops = 0) {
2909
+ if (!node || hops > MAX_HOPS) return null;
2910
+ if (t5.isCallExpression(node)) {
2911
+ for (const arg of node.arguments) {
2912
+ if (t5.isIdentifier(arg) || t5.isMemberExpression(arg)) {
2913
+ const inner = this.componentFile(file2, arg, hops + 1);
2914
+ if (inner) return inner;
2915
+ }
2916
+ }
2917
+ return null;
2918
+ }
2919
+ if (t5.isTSAsExpression(node)) return this.componentFile(file2, node.expression, hops + 1);
2920
+ const chain = memberChain(node);
2921
+ if (!chain) return null;
2922
+ const origin = this.resolveBinding(file2, chain.root);
2923
+ if (!origin) return null;
2924
+ if (chain.path.length === 0) return origin.file;
2925
+ const init = this.topLevelInit(origin.file, origin.name);
2926
+ if (init && t5.isObjectExpression(init)) {
2927
+ const prop = objectProperty(init, chain.path[0]);
2928
+ if (prop) return this.componentFile(origin.file, prop, hops + 1);
2929
+ }
2930
+ const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
2931
+ return viaExport?.file ?? null;
2932
+ }
2933
+ /**
2934
+ * Os arquivos DO PROJETO que este arquivo renderiza como JSX.
2935
+ *
2936
+ * `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
2937
+ * Import de pacote devolve `null` no `resolve` e fica de fora: componente de
2938
+ * terceiro não tem tela nossa dentro.
2939
+ *
2940
+ * Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
2941
+ * extractor, e um navegador não é conteúdo de tela.
2942
+ */
2943
+ renderedComponentFiles(file2) {
2944
+ const ast = this.parse(file2);
2945
+ if (!ast) return [];
2946
+ const names = /* @__PURE__ */ new Set();
2947
+ const visit = (node) => {
2948
+ if (!node || typeof node !== "object") return;
2949
+ if (t5.isJSXOpeningElement(node) && t5.isJSXIdentifier(node.name)) {
2950
+ const n = node.name.name;
2951
+ if (/^[A-Z]/.test(n)) names.add(n);
2952
+ }
2953
+ for (const key of Object.keys(node)) {
2954
+ const value = node[key];
2955
+ if (Array.isArray(value)) {
2956
+ for (const item of value) visit(item);
2957
+ } else if (value && typeof value === "object" && "type" in value) {
2958
+ visit(value);
2959
+ }
2960
+ }
2961
+ };
2962
+ visit(ast.program);
2963
+ const out = /* @__PURE__ */ new Set();
2964
+ for (const name of names) {
2965
+ const origin = this.resolveBinding(file2, name);
2966
+ if (origin && origin.file !== file2) out.add(origin.file);
2967
+ }
2968
+ return [...out];
2969
+ }
2970
+ };
2971
+ function memberChain(node) {
2972
+ const chain = [];
2973
+ let current = node;
2974
+ while (t5.isMemberExpression(current)) {
2975
+ if (current.computed) {
2976
+ if (!t5.isStringLiteral(current.property)) return null;
2977
+ chain.unshift(current.property.value);
2978
+ } else if (t5.isIdentifier(current.property)) {
2979
+ chain.unshift(current.property.name);
2980
+ } else {
2981
+ return null;
2982
+ }
2983
+ current = current.object;
2984
+ }
2985
+ return t5.isIdentifier(current) ? { root: current.name, path: chain } : null;
2986
+ }
2987
+ function objectProperty(obj, key) {
2988
+ for (const prop of obj.properties) {
2989
+ if (!t5.isObjectProperty(prop)) continue;
2990
+ const name = t5.isIdentifier(prop.key) ? prop.key.name : t5.isStringLiteral(prop.key) ? prop.key.value : null;
2991
+ if (name === key && t5.isExpression(prop.value)) return prop.value;
2992
+ }
2993
+ return null;
2994
+ }
2995
+ function readTsconfigAliases(rootDir) {
2996
+ const file2 = import_node_path.default.join(rootDir, "tsconfig.json");
2997
+ if (!(0, import_node_fs.existsSync)(file2)) return [];
2998
+ let parsed;
2999
+ try {
3000
+ const raw = stripJsonComments((0, import_node_fs.readFileSync)(file2, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
3001
+ parsed = JSON.parse(raw);
3002
+ } catch {
3003
+ return [];
3004
+ }
3005
+ const paths = parsed.compilerOptions?.paths;
3006
+ if (!paths) return [];
3007
+ const baseUrl = import_node_path.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
3008
+ const out = [];
3009
+ for (const [pattern, targets] of Object.entries(paths)) {
3010
+ const first = targets[0];
3011
+ if (typeof first !== "string") continue;
3012
+ out.push({
3013
+ prefix: pattern.replace(/\/?\*$/, ""),
3014
+ target: import_node_path.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
3015
+ });
3016
+ }
3017
+ return out.sort((a, b) => b.prefix.length - a.prefix.length);
3018
+ }
3019
+ function stripJsonComments(text) {
3020
+ let out = "";
3021
+ let inString = false;
3022
+ let escaped = false;
3023
+ for (let i = 0; i < text.length; i++) {
3024
+ const c = text[i];
3025
+ if (inString) {
3026
+ out += c;
3027
+ if (escaped) escaped = false;
3028
+ else if (c === "\\") escaped = true;
3029
+ else if (c === '"') inString = false;
3030
+ continue;
3031
+ }
3032
+ if (c === '"') {
3033
+ inString = true;
3034
+ out += c;
3035
+ continue;
3036
+ }
3037
+ if (c === "/" && text[i + 1] === "/") {
3038
+ while (i < text.length && text[i] !== "\n") i++;
3039
+ out += "\n";
3040
+ continue;
3041
+ }
3042
+ if (c === "/" && text[i + 1] === "*") {
3043
+ i += 2;
3044
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3045
+ i++;
3046
+ continue;
3047
+ }
3048
+ out += c;
3049
+ }
3050
+ return out;
3051
+ }
3052
+ function readWorkspacePackages(rootDir) {
3053
+ const start = import_node_path.default.resolve(rootDir);
3054
+ let dir = start;
3055
+ for (let i = 0; i < 6; i++) {
3056
+ const globs = workspaceGlobs(dir);
3057
+ if (globs.length > 0) {
3058
+ const packages = expandWorkspaceGlobs(dir, globs);
3059
+ const containsApp = packages.some(
3060
+ (p) => start === p.dir || start.startsWith(p.dir + import_node_path.default.sep)
3061
+ );
3062
+ return containsApp ? packages : [];
3063
+ }
3064
+ const parent = import_node_path.default.dirname(dir);
3065
+ if (parent === dir) break;
3066
+ dir = parent;
3067
+ }
3068
+ return [];
3069
+ }
3070
+ function workspaceGlobs(dir) {
3071
+ const pnpm = import_node_path.default.join(dir, "pnpm-workspace.yaml");
3072
+ if ((0, import_node_fs.existsSync)(pnpm)) {
3073
+ try {
3074
+ const lines = (0, import_node_fs.readFileSync)(pnpm, "utf-8").split(/\r?\n/);
3075
+ const out = [];
3076
+ let inPackages = false;
3077
+ for (const line of lines) {
3078
+ if (/^packages:/.test(line)) {
3079
+ inPackages = true;
3080
+ continue;
3081
+ }
3082
+ if (inPackages) {
3083
+ const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
3084
+ if (m) out.push(m[1].trim());
3085
+ else if (/^\S/.test(line)) break;
3086
+ }
3087
+ }
3088
+ if (out.length) return out;
3089
+ } catch {
3090
+ }
3091
+ }
3092
+ const pkgPath = import_node_path.default.join(dir, "package.json");
3093
+ if (!(0, import_node_fs.existsSync)(pkgPath)) return [];
3094
+ try {
3095
+ const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8"));
3096
+ const ws = pkg.workspaces;
3097
+ if (Array.isArray(ws)) return ws;
3098
+ if (ws && Array.isArray(ws.packages)) return ws.packages;
3099
+ } catch {
3100
+ }
3101
+ return [];
3102
+ }
3103
+ function expandWorkspaceGlobs(root, globs) {
3104
+ const out = [];
3105
+ const add = (dir) => {
3106
+ const pkgPath = import_node_path.default.join(dir, "package.json");
3107
+ if (!(0, import_node_fs.existsSync)(pkgPath)) return;
3108
+ try {
3109
+ const name = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8")).name;
3110
+ if (name) out.push({ name, dir });
3111
+ } catch {
3112
+ }
3113
+ };
3114
+ for (const glob of globs) {
3115
+ if (glob.endsWith("/*")) {
3116
+ const parent = import_node_path.default.join(root, glob.slice(0, -2));
3117
+ let entries = [];
3118
+ try {
3119
+ entries = (0, import_node_fs.readdirSync)(parent);
3120
+ } catch {
3121
+ continue;
3122
+ }
3123
+ for (const entry of entries) {
3124
+ const dir = import_node_path.default.join(parent, entry);
3125
+ try {
3126
+ if ((0, import_node_fs.statSync)(dir).isDirectory()) add(dir);
3127
+ } catch {
3128
+ }
3129
+ }
3130
+ } else if (!glob.includes("*")) {
3131
+ add(import_node_path.default.join(root, glob));
3132
+ }
3133
+ }
3134
+ return out.sort((a, b) => b.name.length - a.name.length);
3135
+ }
3136
+
3137
+ // src/ast/navigation/expo-router.ts
3138
+ var import_node_fs2 = require("fs");
3139
+ var import_node_path2 = __toESM(require("path"));
3140
+ var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
3141
+ var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
3142
+ var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
3143
+ function baseName(file2) {
3144
+ let name = file2;
3145
+ for (const ext of ROUTE_EXTENSIONS) {
3146
+ if (name.endsWith(ext)) {
3147
+ name = name.slice(0, -ext.length);
3148
+ break;
3149
+ }
3150
+ }
3151
+ for (const suffix of PLATFORM_SUFFIXES) {
3152
+ if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
3153
+ }
3154
+ return name;
3155
+ }
3156
+ function isRouteFile(file2) {
3157
+ return ROUTE_EXTENSIONS.some((ext) => file2.endsWith(ext));
3158
+ }
3159
+ function isSpecial(base) {
3160
+ if (base === "_layout") return true;
3161
+ if (base.startsWith("+")) return base !== "+not-found";
3162
+ return base.startsWith("_");
3163
+ }
3164
+ function layoutType(file2) {
3165
+ try {
3166
+ const src = (0, import_node_fs2.readFileSync)(file2, "utf-8");
3167
+ if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
3168
+ if (/<Drawer\b/.test(src)) return "drawer";
3169
+ } catch {
3170
+ }
3171
+ return "stack";
3172
+ }
3173
+ function findLayout(dir) {
3174
+ for (const ext of ROUTE_EXTENSIONS) {
3175
+ const candidate = import_node_path2.default.join(dir, "_layout" + ext);
3176
+ if ((0, import_node_fs2.existsSync)(candidate)) return candidate;
3177
+ }
3178
+ return null;
3179
+ }
3180
+ function analyzeExpoRouter(appRoot) {
3181
+ let routeDir = null;
3182
+ for (const candidate of ROUTE_DIR_CANDIDATES) {
3183
+ const full = import_node_path2.default.join(appRoot, candidate);
3184
+ if ((0, import_node_fs2.existsSync)(full) && findLayout(full)) {
3185
+ routeDir = full;
3186
+ break;
3187
+ }
3188
+ }
3189
+ if (!routeDir) return null;
3190
+ const routes = [];
3191
+ const navigators = /* @__PURE__ */ new Map();
3192
+ const seen = /* @__PURE__ */ new Set();
3193
+ const walk = (dir, segments, navigator) => {
3194
+ const layout = findLayout(dir);
3195
+ const current = layout ? segments.join("/") || "/" : navigator;
3196
+ if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
3197
+ let entries;
3198
+ try {
3199
+ entries = (0, import_node_fs2.readdirSync)(dir);
3200
+ } catch {
3201
+ return;
3202
+ }
3203
+ for (const entry of entries.sort()) {
3204
+ const full = import_node_path2.default.join(dir, entry);
3205
+ let isDir = false;
3206
+ try {
3207
+ isDir = (0, import_node_fs2.statSync)(full).isDirectory();
3208
+ } catch {
3209
+ continue;
3210
+ }
3211
+ if (isDir) {
3212
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
3213
+ walk(full, [...segments, entry], current);
3214
+ continue;
3215
+ }
3216
+ if (!isRouteFile(entry)) continue;
3217
+ const base = baseName(entry);
3218
+ if (isSpecial(base)) continue;
3219
+ const routeSegments = base === "index" ? segments : [...segments, base];
3220
+ const name = "/" + routeSegments.join("/");
3221
+ if (seen.has(name)) continue;
3222
+ seen.add(name);
3223
+ routes.push({
3224
+ name,
3225
+ componentFile: full,
3226
+ navigatorName: current,
3227
+ navigatorType: navigators.get(current) ?? "stack",
3228
+ params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
3229
+ });
3230
+ }
3231
+ };
3232
+ walk(routeDir, [], "/");
3233
+ return {
3234
+ routeDir: import_node_path2.default.relative(appRoot, routeDir),
3235
+ routes,
3236
+ navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
3237
+ };
3238
+ }
3239
+
3240
+ // src/analyzers/NavigationAnalyzer.ts
3241
+ var NAVIGATOR_FACTORIES = {
3242
+ createStackNavigator: "stack",
3243
+ createNativeStackNavigator: "stack",
3244
+ createBottomTabNavigator: "tab",
3245
+ createTabNavigator: "tab",
3246
+ createMaterialTopTabNavigator: "tab",
3247
+ createMaterialBottomTabNavigator: "tab",
3248
+ createDrawerNavigator: "drawer"
3249
+ };
3250
+ var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
3251
+ function unwrapStaticScreen(value) {
3252
+ if (t6.isCallExpression(value)) {
3253
+ const arg = value.arguments[0];
3254
+ return arg ? unwrapStaticScreen(arg) : null;
3255
+ }
3256
+ if (t6.isObjectExpression(value)) {
3257
+ for (const prop of value.properties) {
3258
+ if (!t6.isObjectProperty(prop)) continue;
3259
+ const key = t6.isIdentifier(prop.key) ? prop.key.name : null;
3260
+ if (key === "screen" && t6.isExpression(prop.value)) return prop.value;
3261
+ }
3262
+ return null;
3263
+ }
3264
+ return t6.isExpression(value) ? value : null;
3265
+ }
3266
+ function inferNavigatorType(factoryName) {
3267
+ if (/drawer/i.test(factoryName)) return "drawer";
3268
+ if (/tab/i.test(factoryName)) return "tab";
3269
+ return "stack";
3270
+ }
3271
+ var NAVIGATION_EVIDENCE = new RegExp(
3272
+ // `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
3273
+ // não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
3274
+ // `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
3275
+ // que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
3276
+ // junção entre arquivos nunca chega a ser tentada.
3277
+ // Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
3278
+ // fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
3279
+ // seis navegadores do `bluesky`.
3280
+ `create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
3281
+ );
2643
3282
  var NavigationAnalyzer = class {
2644
3283
  config;
2645
3284
  /** Extra glob patterns for navigation file discovery (added to defaults) */
2646
3285
  navigationInclude;
2647
3286
  /** Extra glob patterns to exclude from navigation analysis */
2648
3287
  navigationExclude;
3288
+ /** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
3289
+ graph;
3290
+ /**
3291
+ * Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
3292
+ * de navegação e a árvore de telas; sem ela as duas metades do documento
3293
+ * falam de coisas diferentes. Populada por `analyze()`.
3294
+ */
3295
+ routeTargets = /* @__PURE__ */ new Map();
3296
+ /**
3297
+ * Arquivos que DECLARAM uma fábrica de navegador.
3298
+ *
3299
+ * Um navegador aninhado é montado como `component=` de uma rota — no
3300
+ * `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
3301
+ * Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
3302
+ * exatamente o falso positivo que o corpus existe para não repetir. Um
3303
+ * navegador é um contêiner de rotas; a tela está um nível abaixo.
3304
+ *
3305
+ * Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
3306
+ * RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
3307
+ * `DetailViewStack` num arquivo e escreve o JSX em
3308
+ * `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
3309
+ * segundo entrar como tela.
3310
+ */
3311
+ navigatorFiles = /* @__PURE__ */ new Set();
3312
+ /** Preenchido por `analyze()`. Vazio antes disso. */
3313
+ diagnostics = {
3314
+ filesScanned: 0,
3315
+ filesWithEvidence: 0,
3316
+ navigatorsDeclared: 0,
3317
+ navigatorsWithJsx: 0,
3318
+ routes: 0,
3319
+ routesLinkedToFile: 0
3320
+ };
2649
3321
  constructor(config, options) {
2650
3322
  this.config = config;
3323
+ this.graph = new ModuleGraph(config.rootDir);
2651
3324
  this.navigationInclude = options?.navigationInclude ?? [];
2652
3325
  this.navigationExclude = options?.navigationExclude ?? [];
2653
3326
  }
2654
3327
  /** Build the full navigation graph */
2655
3328
  async analyze() {
2656
3329
  const navigationFiles = await this.findNavigationFiles();
2657
- const parsedNavigators = [];
2658
3330
  const typeExports = [];
3331
+ this.routeTargets.clear();
3332
+ const declarations = /* @__PURE__ */ new Map();
3333
+ const evidenceFiles = [];
3334
+ this.navigatorFiles.clear();
2659
3335
  for (const filePath of navigationFiles) {
2660
3336
  try {
2661
3337
  const content = await import_fs4.promises.readFile(filePath, "utf-8");
2662
- const navigators = this.parseNavigators(content, filePath);
2663
- const types = this.parseParamTypes(content, filePath);
2664
- parsedNavigators.push(...navigators);
2665
- typeExports.push(...types);
3338
+ if (!NAVIGATION_EVIDENCE.test(content)) continue;
3339
+ evidenceFiles.push(filePath);
3340
+ for (const decl of this.collectDeclarations(filePath)) {
3341
+ declarations.set(`${decl.file}#${decl.name}`, decl);
3342
+ if (decl.confident) this.navigatorFiles.add(decl.file);
3343
+ }
3344
+ typeExports.push(...this.parseParamTypes(content, filePath));
2666
3345
  } catch (error2) {
2667
3346
  console.warn(`Failed to parse ${filePath}:`, error2);
2668
3347
  }
2669
3348
  }
3349
+ const parsedNavigators = [];
3350
+ for (const filePath of evidenceFiles) {
3351
+ try {
3352
+ parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
3353
+ } catch (error2) {
3354
+ console.warn(`Failed to parse navigators in ${filePath}:`, error2);
3355
+ }
3356
+ }
3357
+ let detachedScreens = 0;
3358
+ for (const filePath of evidenceFiles) {
3359
+ try {
3360
+ for (const detached of this.parseDetachedScreens(filePath, declarations)) {
3361
+ const existing = parsedNavigators.find(
3362
+ (nav) => nav.name === detached.name && nav.type === detached.type
3363
+ );
3364
+ if (!existing) {
3365
+ parsedNavigators.push(detached);
3366
+ detachedScreens += detached.screens.length;
3367
+ continue;
3368
+ }
3369
+ const seen = new Set(existing.screens.map((screen) => screen.name));
3370
+ for (const screen of detached.screens) {
3371
+ if (seen.has(screen.name)) continue;
3372
+ seen.add(screen.name);
3373
+ existing.screens.push(screen);
3374
+ detachedScreens++;
3375
+ }
3376
+ }
3377
+ } catch (error2) {
3378
+ console.warn(`Failed to parse detached screens in ${filePath}:`, error2);
3379
+ }
3380
+ }
3381
+ const expo = analyzeExpoRouter(this.config.rootDir);
3382
+ if (expo) {
3383
+ console.log(
3384
+ `[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
3385
+ );
3386
+ this.diagnostics.fileBasedRouter = {
3387
+ kind: "expo-router",
3388
+ routeDir: expo.routeDir,
3389
+ routes: expo.routes.length
3390
+ };
3391
+ const byNavigator = /* @__PURE__ */ new Map();
3392
+ for (const nav of expo.navigators) {
3393
+ byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
3394
+ }
3395
+ for (const route of expo.routes) {
3396
+ const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
3397
+ name: route.navigatorName,
3398
+ type: route.navigatorType,
3399
+ screens: []
3400
+ }).get(route.navigatorName);
3401
+ navigator.screens.push({
3402
+ name: route.name,
3403
+ navigatorName: route.navigatorName,
3404
+ navigatorType: route.navigatorType,
3405
+ componentFile: route.componentFile,
3406
+ ...route.params.length ? {
3407
+ params: route.params.map((name) => ({ name, type: "string", required: true }))
3408
+ } : {}
3409
+ });
3410
+ this.routeTargets.set(route.name, route.componentFile);
3411
+ }
3412
+ parsedNavigators.push(...byNavigator.values());
3413
+ }
3414
+ let staticOnly = 0;
3415
+ for (const decl of declarations.values()) {
3416
+ if (!decl.confident || !decl.staticScreens?.length) continue;
3417
+ const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
3418
+ if (existing) {
3419
+ const seen = new Set(existing.screens.map((s) => s.name));
3420
+ for (const screen of decl.staticScreens) {
3421
+ if (!seen.has(screen.name)) existing.screens.push(screen);
3422
+ }
3423
+ continue;
3424
+ }
3425
+ parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
3426
+ staticOnly++;
3427
+ }
3428
+ const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
3429
+ const uniqueRoutes = new Set(
3430
+ parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
3431
+ );
3432
+ this.diagnostics = {
3433
+ ...this.diagnostics,
3434
+ filesScanned: navigationFiles.length,
3435
+ filesWithEvidence: evidenceFiles.length,
3436
+ navigatorsDeclared: confidentDeclarations,
3437
+ navigatorsWithJsx: parsedNavigators.length,
3438
+ routes: uniqueRoutes.size,
3439
+ // `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
3440
+ // registrada por um navegador que não sobreviveu. O mínimo evita a
3441
+ // cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
3442
+ routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
3443
+ };
3444
+ console.log(
3445
+ `[NavigationAnalyzer] ${staticOnly} navegador(es) s\xF3 de config est\xE1tica \xB7 ${detachedScreens} rota(s) fora de um <X.Navigator> \xB7 ${navigationFiles.length} arquivos varridos, ${evidenceFiles.length} com evid\xEAncia, ${confidentDeclarations} navegadores declarados, ${parsedNavigators.length} com JSX, ${this.routeTargets.size} rotas ligadas a um arquivo`
3446
+ );
2670
3447
  this.attachParamsToNavigators(parsedNavigators, typeExports);
2671
3448
  return this.buildNavigationGraph(parsedNavigators);
2672
3449
  }
2673
- /** Find all navigation-related files */
3450
+ /** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
3451
+ collectDeclarations(filePath) {
3452
+ const ast = this.graph.parse(filePath);
3453
+ if (!ast) return [];
3454
+ const out = /* @__PURE__ */ new Map();
3455
+ (0, import_traverse5.default)(ast, {
3456
+ CallExpression: (nodePath) => {
3457
+ const callee = nodePath.node.callee;
3458
+ if (!t6.isIdentifier(callee)) return;
3459
+ const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
3460
+ const type = known ?? inferNavigatorType(callee.name);
3461
+ const confident = known !== null;
3462
+ let up = nodePath.parentPath;
3463
+ for (let i = 0; i < 4 && up; i++) {
3464
+ if (t6.isVariableDeclarator(up.node)) break;
3465
+ if (!t6.isMemberExpression(up.node) && !t6.isCallExpression(up.node)) {
3466
+ up = null;
3467
+ break;
3468
+ }
3469
+ up = up.parentPath;
3470
+ }
3471
+ const declarator = up?.node;
3472
+ if (!declarator || !t6.isVariableDeclarator(declarator) || !t6.isIdentifier(declarator.id)) {
3473
+ return;
3474
+ }
3475
+ const name = declarator.id.name;
3476
+ if (out.get(name)?.confident) return;
3477
+ const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
3478
+ out.set(name, {
3479
+ file: filePath,
3480
+ name,
3481
+ type,
3482
+ confident,
3483
+ ...staticScreens.length ? { staticScreens } : {}
3484
+ });
3485
+ }
3486
+ });
3487
+ return [...out.values()];
3488
+ }
3489
+ /**
3490
+ * As rotas de `createXNavigator({ screens: { … } })`.
3491
+ *
3492
+ * Quatro formas no corpus, todas em `rocketchat`:
3493
+ * `NewServerView` — shorthand, nome = componente
3494
+ * `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
3495
+ * `SelectListView: SelectListViewScreen` — identificador direto
3496
+ * `ChatsStackNavigator: ChatsStack` — navegador aninhado
3497
+ *
3498
+ * `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
3499
+ * e agrupar rotas não muda o que elas são.
3500
+ */
3501
+ parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
3502
+ if (!config || !t6.isObjectExpression(config) || depth > 4) return [];
3503
+ const out = [];
3504
+ for (const prop of config.properties) {
3505
+ if (!t6.isObjectProperty(prop)) continue;
3506
+ const key = t6.isIdentifier(prop.key) ? prop.key.name : t6.isStringLiteral(prop.key) ? prop.key.value : null;
3507
+ if (!key) continue;
3508
+ if (key === "groups" && t6.isObjectExpression(prop.value)) {
3509
+ for (const group of prop.value.properties) {
3510
+ if (!t6.isObjectProperty(group) || !t6.isExpression(group.value)) continue;
3511
+ out.push(
3512
+ ...this.parseStaticScreens(
3513
+ filePath,
3514
+ group.value,
3515
+ navigatorName,
3516
+ navigatorType,
3517
+ depth + 1
3518
+ )
3519
+ );
3520
+ }
3521
+ continue;
3522
+ }
3523
+ if (key !== "screens" || !t6.isObjectExpression(prop.value)) continue;
3524
+ for (const entry of prop.value.properties) {
3525
+ if (!t6.isObjectProperty(entry)) continue;
3526
+ const routeName = t6.isIdentifier(entry.key) ? entry.key.name : t6.isStringLiteral(entry.key) ? entry.key.value : null;
3527
+ if (!routeName) continue;
3528
+ const componentExpr = unwrapStaticScreen(entry.value);
3529
+ const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
3530
+ if (componentFile) this.routeTargets.set(routeName, componentFile);
3531
+ out.push({
3532
+ name: routeName,
3533
+ navigatorName,
3534
+ navigatorType,
3535
+ ...componentFile ? { componentFile } : {}
3536
+ });
3537
+ }
3538
+ }
3539
+ return out;
3540
+ }
3541
+ /**
3542
+ * Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
3543
+ * certo.
3544
+ *
3545
+ * POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
3546
+ * `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
3547
+ * convenção de caminho. Medido contra 20 apps React Native de terceiros
3548
+ * (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
3549
+ * app não ter navegação:
3550
+ *
3551
+ * - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
3552
+ * - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
3553
+ * sensível a caixa;
3554
+ * - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
3555
+ * pedia um arquivo chamado `routes*`;
3556
+ * - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
3557
+ * - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
3558
+ *
3559
+ * Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
3560
+ * globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
3561
+ * O que segura o custo aqui é o portão por evidência em `analyze()`, que só
3562
+ * parseia arquivo cujo texto menciona uma fábrica de navegador.
3563
+ *
3564
+ * Os globs antigos continuam na lista: se um projeto restringir `include`, o
3565
+ * que era encontrado antes continua sendo.
3566
+ */
2674
3567
  async findNavigationFiles() {
2675
3568
  const patterns = [
3569
+ ...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
3570
+ // Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
2676
3571
  "**/navigation/**/*.{ts,tsx}",
2677
3572
  "**/navigator*.{ts,tsx}",
2678
3573
  "**/routes*.{ts,tsx}",
@@ -2683,91 +3578,188 @@ var NavigationAnalyzer = class {
2683
3578
  "**/node_modules/**",
2684
3579
  "**/dist/**",
2685
3580
  "**/build/**",
3581
+ // Um navegador declarado dentro de um teste é fixture, não a navegação
3582
+ // do app. Isto passou a importar quando a varredura deixou de ser por
3583
+ // caminho: em `expensify`, os únicos arquivos que o glob antigo
3584
+ // alcançava eram três de `tests/`.
3585
+ "**/__tests__/**",
3586
+ "**/test/**",
3587
+ "**/tests/**",
3588
+ "**/*.test.{ts,tsx,js,jsx}",
3589
+ "**/*.tests.{ts,tsx,js,jsx}",
3590
+ "**/*.spec.{ts,tsx,js,jsx}",
3591
+ // Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
3592
+ // `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
3593
+ // Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
3594
+ // destinos que existem no teste e não no app, para os quais o agente
3595
+ // tentaria navegar.
3596
+ "**/*Test.{ts,tsx,js,jsx}",
3597
+ "**/*Tests.{ts,tsx,js,jsx}",
2686
3598
  ...this.config.exclude || [],
2687
3599
  // §5: Add user-configured navigation excludes
2688
3600
  ...this.navigationExclude
2689
3601
  ];
2690
- const files = await (0, import_fast_glob2.default)(patterns, {
3602
+ const files = await globSorted(patterns, {
2691
3603
  cwd: this.config.rootDir,
2692
3604
  ignore: excludePatterns
2693
3605
  });
2694
3606
  return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
2695
3607
  }
2696
- /** Parse navigator definitions from a file */
2697
- parseNavigators(content, filePath) {
2698
- const navigators = [];
2699
- try {
2700
- const ast = parser.parse(content, {
2701
- sourceType: "module",
2702
- plugins: [
2703
- "jsx",
2704
- "typescript",
2705
- ...this.config.parserPlugins || []
2706
- ]
2707
- });
2708
- const navigatorCalls = /* @__PURE__ */ new Map();
2709
- const screensByNavigator = /* @__PURE__ */ new Map();
2710
- (0, import_traverse5.default)(ast, {
2711
- // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
2712
- CallExpression: (nodePath) => {
2713
- const { node } = nodePath;
2714
- const callee = node.callee;
2715
- let navigatorType = null;
2716
- if (t5.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
2717
- navigatorType = "stack";
2718
- } else if (t5.isIdentifier(callee) && callee.name === "createStackNavigator") {
2719
- navigatorType = "stack";
2720
- } else if (t5.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
2721
- navigatorType = "tab";
2722
- } else if (t5.isIdentifier(callee) && callee.name === "createTabNavigator") {
2723
- navigatorType = "tab";
2724
- } else if (t5.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
2725
- navigatorType = "drawer";
2726
- }
2727
- if (navigatorType) {
2728
- const parent = nodePath.parent;
2729
- if (t5.isVariableDeclarator(parent) && t5.isIdentifier(parent.id)) {
2730
- const varName = parent.id.name;
2731
- navigatorCalls.set(varName, {
2732
- name: varName,
2733
- type: navigatorType,
2734
- screens: []
2735
- });
2736
- }
2737
- }
2738
- },
2739
- // Detect Stack.Navigator / Tab.Navigator JSX elements
2740
- JSXElement: (nodePath) => {
2741
- const { node } = nodePath;
2742
- const openingElement = node.openingElement;
2743
- if (t5.isJSXMemberExpression(openingElement.name) && t5.isJSXIdentifier(openingElement.name.object)) {
2744
- const objectName = openingElement.name.object.name;
2745
- const propertyName = t5.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2746
- if (propertyName === "Navigator") {
2747
- const navigator = navigatorCalls.get(objectName);
2748
- if (navigator) {
2749
- const initialRouteAttr = openingElement.attributes.find(
2750
- (attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2751
- );
2752
- if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
2753
- navigator.initialRouteName = initialRouteAttr.value.value;
2754
- }
2755
- const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
2756
- screensByNavigator.set(objectName, screens);
2757
- }
2758
- }
2759
- }
3608
+ /**
3609
+ * Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
3610
+ * que pode estar aqui ou em qualquer arquivo que este importe.
3611
+ */
3612
+ parseNavigatorUsages(filePath, declarations) {
3613
+ const ast = this.graph.parse(filePath);
3614
+ if (!ast) return [];
3615
+ const found = /* @__PURE__ */ new Map();
3616
+ (0, import_traverse5.default)(ast, {
3617
+ JSXElement: (nodePath) => {
3618
+ const { node } = nodePath;
3619
+ const openingElement = node.openingElement;
3620
+ if (!t6.isJSXMemberExpression(openingElement.name) || !t6.isJSXIdentifier(openingElement.name.object)) {
3621
+ return;
2760
3622
  }
2761
- });
2762
- navigatorCalls.forEach((navigator) => {
2763
- const screens = screensByNavigator.get(navigator.name) || [];
2764
- navigator.screens = screens;
2765
- navigators.push(navigator);
2766
- });
2767
- } catch (error2) {
2768
- console.warn(`Failed to parse navigators in ${filePath}:`, error2);
3623
+ const objectName = openingElement.name.object.name;
3624
+ const propertyName = t6.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
3625
+ if (propertyName !== "Navigator") return;
3626
+ const decl = this.resolveNavigator(filePath, objectName, declarations);
3627
+ if (!decl) return;
3628
+ const navigator = found.get(objectName) ?? {
3629
+ name: objectName,
3630
+ type: decl.type,
3631
+ screens: []
3632
+ };
3633
+ const initialRouteAttr = openingElement.attributes.find(
3634
+ (attr) => t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
3635
+ );
3636
+ if (t6.isJSXAttribute(initialRouteAttr)) {
3637
+ const initial = this.attributeString(filePath, initialRouteAttr);
3638
+ if (initial) navigator.initialRouteName = initial;
3639
+ }
3640
+ this.navigatorFiles.add(filePath);
3641
+ this.navigatorFiles.add(decl.file);
3642
+ navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
3643
+ found.set(objectName, navigator);
3644
+ }
3645
+ });
3646
+ return [...found.values()];
3647
+ }
3648
+ /**
3649
+ * Rotas declaradas FORA de qualquer `<X.Navigator>`.
3650
+ *
3651
+ * O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
3652
+ * que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
3653
+ * dois são 171 rotas invisíveis:
3654
+ *
3655
+ * `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
3656
+ * <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
3657
+ * dentro de seis navegadores diferentes. 70 rotas.
3658
+ * `comapeo` — `export const createAppScreens = ({intl}) => (<>
3659
+ * <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
3660
+ * num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
3661
+ *
3662
+ * A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
3663
+ * navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
3664
+ * literal do fonte (ou constante que o resolvedor segue), e `X` precisa
3665
+ * chegar a uma declaração de navegador que já existe. O que não resolve não
3666
+ * vira nada.
3667
+ *
3668
+ * O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
3669
+ * fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
3670
+ *
3671
+ * DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
3672
+ * primeira é a de sempre (declaração local, ou `import` seguido até a
3673
+ * origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
3674
+ * parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
3675
+ * o navegador `Flat` é o próprio app, no fonte.
3676
+ *
3677
+ * O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
3678
+ * contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
3679
+ * cinco stacks que também chamam a mesma função. Seguir os seis pontos de
3680
+ * chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
3681
+ * da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
3682
+ */
3683
+ parseDetachedScreens(filePath, declarations) {
3684
+ const ast = this.graph.parse(filePath);
3685
+ if (!ast) return [];
3686
+ const byNavigator = /* @__PURE__ */ new Map();
3687
+ const resolved = /* @__PURE__ */ new Map();
3688
+ (0, import_traverse5.default)(ast, {
3689
+ JSXElement: (nodePath) => {
3690
+ const name = nodePath.node.openingElement.name;
3691
+ if (!t6.isJSXMemberExpression(name) || !t6.isJSXIdentifier(name.object)) return;
3692
+ if (!t6.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
3693
+ const insideNavigator = nodePath.findParent((parent) => {
3694
+ if (!parent.isJSXElement()) return false;
3695
+ const parentName = parent.node.openingElement.name;
3696
+ return t6.isJSXMemberExpression(parentName) && t6.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
3697
+ });
3698
+ if (insideNavigator) return;
3699
+ const local = name.object.name;
3700
+ if (!resolved.has(local)) {
3701
+ resolved.set(
3702
+ local,
3703
+ this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
3704
+ );
3705
+ }
3706
+ const decl = resolved.get(local);
3707
+ if (!decl) return;
3708
+ const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
3709
+ if (!screen) return;
3710
+ const nav = byNavigator.get(decl.name) ?? {
3711
+ name: decl.name,
3712
+ type: decl.type,
3713
+ screens: []
3714
+ };
3715
+ if (!nav.screens.some((existing) => existing.name === screen.name))
3716
+ nav.screens.push(screen);
3717
+ byNavigator.set(decl.name, nav);
3718
+ }
3719
+ });
3720
+ return [...byNavigator.values()];
3721
+ }
3722
+ /**
3723
+ * De um `X` usado como `<X.Screen>` até a declaração do navegador.
3724
+ *
3725
+ * Além do caminho normal — declaração local ou `import` seguido até a origem
3726
+ * — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
3727
+ * `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
3728
+ */
3729
+ resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
3730
+ const direct = this.resolveNavigator(filePath, localName, declarations);
3731
+ if (direct) return direct;
3732
+ const binding = nodePath.scope.getBinding(localName);
3733
+ if (!binding || binding.kind !== "param") return null;
3734
+ const param = binding.path.node;
3735
+ if (!t6.isIdentifier(param) || !t6.isTSTypeAnnotation(param.typeAnnotation)) return null;
3736
+ const annotation = param.typeAnnotation.typeAnnotation;
3737
+ if (!t6.isTSTypeQuery(annotation) || !t6.isIdentifier(annotation.exprName)) return null;
3738
+ return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
3739
+ }
3740
+ /**
3741
+ * De `<X.Navigator>` até a declaração de `X`.
3742
+ *
3743
+ * Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
3744
+ * é definido. Resolver pelo import é o que torna a junção entre arquivos
3745
+ * segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
3746
+ * é o arquivo de DEFINIÇÃO.
3747
+ */
3748
+ resolveNavigator(filePath, localName, declarations) {
3749
+ const local = declarations.get(`${filePath}#${localName}`);
3750
+ if (local) return local;
3751
+ const origin = this.graph.resolveBinding(filePath, localName);
3752
+ if (!origin) return null;
3753
+ return declarations.get(`${origin.file}#${origin.name}`) ?? null;
3754
+ }
3755
+ /** O valor string de um atributo JSX, resolvendo constante importada. */
3756
+ attributeString(filePath, attr) {
3757
+ const value = attr.value;
3758
+ if (t6.isStringLiteral(value)) return value.value;
3759
+ if (t6.isJSXExpressionContainer(value) && t6.isExpression(value.expression)) {
3760
+ return this.graph.stringConstant(filePath, value.expression);
2769
3761
  }
2770
- return navigators;
3762
+ return null;
2771
3763
  }
2772
3764
  /**
2773
3765
  * Is this JSX element `<navigatorVarName.MEMBER …>`?
@@ -2778,9 +3770,9 @@ var NavigationAnalyzer = class {
2778
3770
  // `never`. It is a question about the member name, not about the node
2779
3771
  // kind.
2780
3772
  isNavigatorMember(node, navigatorVarName, member) {
2781
- if (!t5.isJSXElement(node)) return false;
3773
+ if (!t6.isJSXElement(node)) return false;
2782
3774
  const name = node.openingElement.name;
2783
- return t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t5.isJSXIdentifier(name.property) && name.property.name === member;
3775
+ return t6.isJSXMemberExpression(name) && t6.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t6.isJSXIdentifier(name.property) && name.property.name === member;
2784
3776
  }
2785
3777
  /**
2786
3778
  * Flatten a navigator's JSX children into the `<X.Screen>` elements they
@@ -2811,37 +3803,37 @@ var NavigationAnalyzer = class {
2811
3803
  found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
2812
3804
  return;
2813
3805
  }
2814
- if (t5.isJSXElement(node)) {
3806
+ if (t6.isJSXElement(node)) {
2815
3807
  const name = node.openingElement.name;
2816
- const isForeignNavigator = t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.property) && name.property.name === "Navigator";
3808
+ const isForeignNavigator = t6.isJSXMemberExpression(name) && t6.isJSXIdentifier(name.property) && name.property.name === "Navigator";
2817
3809
  if (isForeignNavigator) return;
2818
3810
  found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2819
3811
  return;
2820
3812
  }
2821
- if (t5.isJSXFragment(node)) {
3813
+ if (t6.isJSXFragment(node)) {
2822
3814
  found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2823
3815
  return;
2824
3816
  }
2825
- if (t5.isJSXExpressionContainer(node)) {
3817
+ if (t6.isJSXExpressionContainer(node)) {
2826
3818
  visit(node.expression);
2827
3819
  return;
2828
3820
  }
2829
- if (t5.isConditionalExpression(node)) {
3821
+ if (t6.isConditionalExpression(node)) {
2830
3822
  visit(node.consequent);
2831
3823
  visit(node.alternate);
2832
3824
  return;
2833
3825
  }
2834
- if (t5.isLogicalExpression(node)) {
3826
+ if (t6.isLogicalExpression(node)) {
2835
3827
  visit(node.left);
2836
3828
  visit(node.right);
2837
3829
  return;
2838
3830
  }
2839
- if (t5.isCallExpression(node)) {
3831
+ if (t6.isCallExpression(node)) {
2840
3832
  for (const arg of node.arguments) {
2841
- if (t5.isArrowFunctionExpression(arg) || t5.isFunctionExpression(arg)) {
2842
- if (t5.isBlockStatement(arg.body)) {
3833
+ if (t6.isArrowFunctionExpression(arg) || t6.isFunctionExpression(arg)) {
3834
+ if (t6.isBlockStatement(arg.body)) {
2843
3835
  for (const stmt of arg.body.body) {
2844
- if (t5.isReturnStatement(stmt)) visit(stmt.argument);
3836
+ if (t6.isReturnStatement(stmt)) visit(stmt.argument);
2845
3837
  }
2846
3838
  } else {
2847
3839
  visit(arg.body);
@@ -2850,11 +3842,11 @@ var NavigationAnalyzer = class {
2850
3842
  }
2851
3843
  return;
2852
3844
  }
2853
- if (t5.isArrayExpression(node)) {
3845
+ if (t6.isArrayExpression(node)) {
2854
3846
  for (const el of node.elements) visit(el);
2855
3847
  return;
2856
3848
  }
2857
- if (t5.isTSAsExpression(node) || t5.isTSNonNullExpression(node)) {
3849
+ if (t6.isTSAsExpression(node) || t6.isTSNonNullExpression(node)) {
2858
3850
  visit(node.expression);
2859
3851
  }
2860
3852
  };
@@ -2862,28 +3854,54 @@ var NavigationAnalyzer = class {
2862
3854
  return found;
2863
3855
  }
2864
3856
  /** Extract screens from a navigator JSX element */
2865
- extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
3857
+ extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
2866
3858
  const screens = [];
2867
3859
  if (!navigatorElement.children) return screens;
2868
3860
  const seen = /* @__PURE__ */ new Set();
2869
3861
  for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
2870
- const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
2871
- if (!screenName || seen.has(screenName)) continue;
2872
- seen.add(screenName);
2873
- screens.push({
2874
- name: screenName,
2875
- navigatorName: navigatorVarName,
2876
- navigatorType
2877
- });
3862
+ const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
3863
+ if (!screen || seen.has(screen.name)) continue;
3864
+ seen.add(screen.name);
3865
+ screens.push(screen);
2878
3866
  }
2879
3867
  return screens;
2880
3868
  }
3869
+ /**
3870
+ * Uma `<X.Screen>` isolada até a rota que ela declara.
3871
+ *
3872
+ * Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
3873
+ * a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
3874
+ * dele — só a forma de chegar até o elemento muda.
3875
+ */
3876
+ parseScreenElement(filePath, element, navigatorName, navigatorType) {
3877
+ const attrs = element.openingElement.attributes;
3878
+ const nameAttr = attrs.find(
3879
+ (a) => t6.isJSXAttribute(a) && t6.isJSXIdentifier(a.name) && a.name.name === "name"
3880
+ );
3881
+ const screenName = t6.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
3882
+ if (!screenName) return null;
3883
+ const componentAttr = attrs.find(
3884
+ (a) => t6.isJSXAttribute(a) && t6.isJSXIdentifier(a.name) && a.name.name === "component"
3885
+ );
3886
+ let componentFile = null;
3887
+ if (t6.isJSXAttribute(componentAttr) && t6.isJSXExpressionContainer(componentAttr.value)) {
3888
+ const expr = componentAttr.value.expression;
3889
+ if (t6.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
3890
+ }
3891
+ if (componentFile) this.routeTargets.set(screenName, componentFile);
3892
+ return {
3893
+ name: screenName,
3894
+ navigatorName,
3895
+ navigatorType,
3896
+ ...componentFile ? { componentFile } : {}
3897
+ };
3898
+ }
2881
3899
  /** Extract string attribute value from JSX attributes */
2882
3900
  extractAttributeValue(attributes, attrName) {
2883
3901
  const attr = attributes.find(
2884
- (a) => t5.isJSXAttribute(a) && t5.isJSXIdentifier(a.name) && a.name.name === attrName
3902
+ (a) => t6.isJSXAttribute(a) && t6.isJSXIdentifier(a.name) && a.name.name === attrName
2885
3903
  );
2886
- if (t5.isJSXAttribute(attr) && t5.isStringLiteral(attr.value)) {
3904
+ if (t6.isJSXAttribute(attr) && t6.isStringLiteral(attr.value)) {
2887
3905
  return attr.value.value;
2888
3906
  }
2889
3907
  return null;
@@ -2892,7 +3910,7 @@ var NavigationAnalyzer = class {
2892
3910
  parseParamTypes(content, filePath) {
2893
3911
  const types = [];
2894
3912
  try {
2895
- const ast = parser.parse(content, {
3913
+ const ast = parser2.parse(content, {
2896
3914
  sourceType: "module",
2897
3915
  plugins: [
2898
3916
  "jsx",
@@ -2973,7 +3991,7 @@ var NavigationAnalyzer = class {
2973
3991
  if (type.type === "TSUndefinedKeyword") return "undefined";
2974
3992
  if (type.type === "TSNullKeyword") return "null";
2975
3993
  if (type.type === "TSUnionType") {
2976
- return type.types.map((t15) => this.typeToString(t15)).join(" | ");
3994
+ return type.types.map((t16) => this.typeToString(t16)).join(" | ");
2977
3995
  }
2978
3996
  if (type.type === "TSTypeLiteral") {
2979
3997
  return "object";
@@ -2994,7 +4012,7 @@ var NavigationAnalyzer = class {
2994
4012
  /** Attach parsed type params to navigator screens */
2995
4013
  attachParamsToNavigators(navigators, types) {
2996
4014
  for (const navigator of navigators) {
2997
- const matchingType = types.find((t15) => t15.type === navigator.type);
4015
+ const matchingType = types.find((t16) => t16.type === navigator.type);
2998
4016
  if (matchingType) {
2999
4017
  for (const screen of navigator.screens) {
3000
4018
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -3072,9 +4090,9 @@ var NavigationAnalyzer = class {
3072
4090
 
3073
4091
  // src/analyzers/ComponentAnalyzer.ts
3074
4092
  var import_fs5 = require("fs");
3075
- var parser2 = __toESM(require("@babel/parser"));
4093
+ var parser3 = __toESM(require("@babel/parser"));
3076
4094
  var import_traverse6 = __toESM(require("@babel/traverse"));
3077
- var t6 = __toESM(require("@babel/types"));
4095
+ var t7 = __toESM(require("@babel/types"));
3078
4096
  var ComponentAnalyzer = class {
3079
4097
  config;
3080
4098
  constructor(config) {
@@ -3084,14 +4102,14 @@ var ComponentAnalyzer = class {
3084
4102
  async analyzeFile(filePath) {
3085
4103
  try {
3086
4104
  const code = (0, import_fs5.readFileSync)(filePath, "utf-8");
3087
- const ast = parser2.parse(code, {
4105
+ const ast = parser3.parse(code, {
3088
4106
  sourceType: "module",
3089
4107
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
3090
4108
  });
3091
4109
  const components = [];
3092
4110
  (0, import_traverse6.default)(ast, {
3093
- JSXElement: (path11) => {
3094
- const component = this.extractComponentFromJSXElement(path11.node);
4111
+ JSXElement: (path13) => {
4112
+ const component = this.extractComponentFromJSXElement(path13.node);
3095
4113
  if (component) {
3096
4114
  components.push(component);
3097
4115
  }
@@ -3120,19 +4138,19 @@ var ComponentAnalyzer = class {
3120
4138
  };
3121
4139
  }
3122
4140
  getElementName(openingElement) {
3123
- if (t6.isJSXIdentifier(openingElement.name)) {
4141
+ if (t7.isJSXIdentifier(openingElement.name)) {
3124
4142
  return openingElement.name.name;
3125
4143
  }
3126
- if (t6.isJSXMemberExpression(openingElement.name)) {
4144
+ if (t7.isJSXMemberExpression(openingElement.name)) {
3127
4145
  const parts = [];
3128
4146
  let current = openingElement.name;
3129
- while (t6.isJSXMemberExpression(current)) {
3130
- if (t6.isJSXIdentifier(current.property)) {
4147
+ while (t7.isJSXMemberExpression(current)) {
4148
+ if (t7.isJSXIdentifier(current.property)) {
3131
4149
  parts.unshift(current.property.name);
3132
4150
  }
3133
4151
  current = current.object;
3134
4152
  }
3135
- if (t6.isJSXIdentifier(current)) {
4153
+ if (t7.isJSXIdentifier(current)) {
3136
4154
  parts.unshift(current.name);
3137
4155
  }
3138
4156
  return parts.join(".");
@@ -3155,13 +4173,13 @@ var ComponentAnalyzer = class {
3155
4173
  extractProps(openingElement) {
3156
4174
  const props = {};
3157
4175
  for (const attr of openingElement.attributes) {
3158
- if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
4176
+ if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
3159
4177
  const propName = attr.name.name;
3160
4178
  if (attr.value === null) {
3161
4179
  props[propName] = "true";
3162
- } else if (t6.isStringLiteral(attr.value)) {
4180
+ } else if (t7.isStringLiteral(attr.value)) {
3163
4181
  props[propName] = attr.value.value;
3164
- } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
4182
+ } else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
3165
4183
  props[propName] = attr.value.expression.value;
3166
4184
  }
3167
4185
  }
@@ -3171,12 +4189,12 @@ var ComponentAnalyzer = class {
3171
4189
  extractAccessibilityProps(openingElement) {
3172
4190
  const result = {};
3173
4191
  for (const attr of openingElement.attributes) {
3174
- if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
4192
+ if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
3175
4193
  const propName = attr.name.name;
3176
4194
  if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
3177
- if (t6.isStringLiteral(attr.value)) {
4195
+ if (t7.isStringLiteral(attr.value)) {
3178
4196
  result[propName] = attr.value.value;
3179
- } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
4197
+ } else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
3180
4198
  result[propName] = attr.value.expression.value;
3181
4199
  }
3182
4200
  }
@@ -3188,7 +4206,7 @@ var ComponentAnalyzer = class {
3188
4206
  if (depth >= maxDepth) return [];
3189
4207
  const extracted = [];
3190
4208
  for (const child of children) {
3191
- if (t6.isJSXElement(child)) {
4209
+ if (t7.isJSXElement(child)) {
3192
4210
  const component = this.extractComponentFromJSXElement(child);
3193
4211
  if (component) {
3194
4212
  extracted.push(component);
@@ -3201,10 +4219,10 @@ var ComponentAnalyzer = class {
3201
4219
 
3202
4220
  // src/analyzers/FormAnalyzer.ts
3203
4221
  var import_fs6 = require("fs");
3204
- var parser3 = __toESM(require("@babel/parser"));
4222
+ var parser4 = __toESM(require("@babel/parser"));
3205
4223
  var import_traverse7 = __toESM(require("@babel/traverse"));
3206
- var t7 = __toESM(require("@babel/types"));
3207
- var path3 = __toESM(require("path"));
4224
+ var t8 = __toESM(require("@babel/types"));
4225
+ var path5 = __toESM(require("path"));
3208
4226
  var FormAnalyzer = class {
3209
4227
  config;
3210
4228
  stateVariables = /* @__PURE__ */ new Map();
@@ -3217,7 +4235,7 @@ var FormAnalyzer = class {
3217
4235
  async analyzeFile(filePath) {
3218
4236
  try {
3219
4237
  const code = (0, import_fs6.readFileSync)(filePath, "utf-8");
3220
- const ast = parser3.parse(code, {
4238
+ const ast = parser4.parse(code, {
3221
4239
  sourceType: "module",
3222
4240
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
3223
4241
  });
@@ -3225,13 +4243,13 @@ var FormAnalyzer = class {
3225
4243
  this.inputElements = [];
3226
4244
  this.submitButtons = [];
3227
4245
  (0, import_traverse7.default)(ast, {
3228
- CallExpression: (path11) => {
3229
- this.extractStateVariables(path11.node);
4246
+ CallExpression: (path13) => {
4247
+ this.extractStateVariables(path13.node);
3230
4248
  }
3231
4249
  });
3232
4250
  (0, import_traverse7.default)(ast, {
3233
- JSXElement: (path11) => {
3234
- this.extractFormElements(path11.node);
4251
+ JSXElement: (path13) => {
4252
+ this.extractFormElements(path13.node);
3235
4253
  }
3236
4254
  });
3237
4255
  const validationRules = this.extractValidationRules(ast);
@@ -3242,7 +4260,7 @@ var FormAnalyzer = class {
3242
4260
  }
3243
4261
  }
3244
4262
  extractStateVariables(node) {
3245
- if (t7.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
4263
+ if (t8.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
3246
4264
  return;
3247
4265
  }
3248
4266
  }
@@ -3263,7 +4281,7 @@ var FormAnalyzer = class {
3263
4281
  extractInputInfo(openingElement) {
3264
4282
  const info2 = { varName: "" };
3265
4283
  for (const attr of openingElement.attributes) {
3266
- if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
4284
+ if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
3267
4285
  const propName = attr.name.name;
3268
4286
  const propValue = this.extractAttributeValue(attr.value);
3269
4287
  switch (propName) {
@@ -3284,7 +4302,7 @@ var FormAnalyzer = class {
3284
4302
  if (propValue) info2.varName = propValue;
3285
4303
  break;
3286
4304
  case "value":
3287
- if (attr.value && t7.isJSXExpressionContainer(attr.value) && t7.isIdentifier(attr.value.expression)) {
4305
+ if (attr.value && t8.isJSXExpressionContainer(attr.value) && t8.isIdentifier(attr.value.expression)) {
3288
4306
  info2.varName = attr.value.expression.name;
3289
4307
  }
3290
4308
  break;
@@ -3296,16 +4314,16 @@ var FormAnalyzer = class {
3296
4314
  extractButtonInfo(openingElement) {
3297
4315
  const info2 = {};
3298
4316
  for (const attr of openingElement.attributes) {
3299
- if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
4317
+ if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
3300
4318
  const propName = attr.name.name;
3301
4319
  if (propName === "title" || propName === "label") {
3302
4320
  info2.label = this.extractAttributeValue(attr.value);
3303
4321
  }
3304
4322
  if (propName === "onPress") {
3305
- if (attr.value && t7.isJSXExpressionContainer(attr.value)) {
3306
- if (t7.isIdentifier(attr.value.expression)) {
4323
+ if (attr.value && t8.isJSXExpressionContainer(attr.value)) {
4324
+ if (t8.isIdentifier(attr.value.expression)) {
3307
4325
  info2.handler = attr.value.expression.name;
3308
- } else if (t7.isArrowFunctionExpression(attr.value.expression) || t7.isFunctionExpression(attr.value.expression)) {
4326
+ } else if (t8.isArrowFunctionExpression(attr.value.expression) || t8.isFunctionExpression(attr.value.expression)) {
3309
4327
  info2.handler = "anonymous";
3310
4328
  }
3311
4329
  }
@@ -3316,14 +4334,14 @@ var FormAnalyzer = class {
3316
4334
  }
3317
4335
  extractAttributeValue(value) {
3318
4336
  if (value === null) return void 0;
3319
- if (t7.isStringLiteral(value)) return value.value;
3320
- if (t7.isJSXExpressionContainer(value) && t7.isStringLiteral(value.expression)) {
4337
+ if (t8.isStringLiteral(value)) return value.value;
4338
+ if (t8.isJSXExpressionContainer(value) && t8.isStringLiteral(value.expression)) {
3321
4339
  return value.expression.value;
3322
4340
  }
3323
4341
  return void 0;
3324
4342
  }
3325
4343
  getElementName(openingElement) {
3326
- if (t7.isJSXIdentifier(openingElement.name)) {
4344
+ if (t8.isJSXIdentifier(openingElement.name)) {
3327
4345
  return openingElement.name.name;
3328
4346
  }
3329
4347
  return null;
@@ -3331,8 +4349,8 @@ var FormAnalyzer = class {
3331
4349
  extractValidationRules(ast) {
3332
4350
  const rules = {};
3333
4351
  (0, import_traverse7.default)(ast, {
3334
- IfStatement: (path11) => {
3335
- const test = path11.node.test;
4352
+ IfStatement: (path13) => {
4353
+ const test = path13.node.test;
3336
4354
  const rule = this.extractRuleFromCondition(test);
3337
4355
  if (rule) {
3338
4356
  const { field, description } = rule;
@@ -3347,36 +4365,36 @@ var FormAnalyzer = class {
3347
4365
  extractRuleFromCondition(test) {
3348
4366
  let fieldName = "";
3349
4367
  let description = "";
3350
- if (t7.isUnaryExpression(test) && test.operator === "!") {
3351
- if (t7.isCallExpression(test.argument)) {
4368
+ if (t8.isUnaryExpression(test) && test.operator === "!") {
4369
+ if (t8.isCallExpression(test.argument)) {
3352
4370
  const callExpr = test.argument;
3353
- if (t7.isMemberExpression(callExpr.callee)) {
4371
+ if (t8.isMemberExpression(callExpr.callee)) {
3354
4372
  const memberExpr = callExpr.callee;
3355
- if (t7.isIdentifier(memberExpr.object)) {
4373
+ if (t8.isIdentifier(memberExpr.object)) {
3356
4374
  fieldName = memberExpr.object.name;
3357
4375
  description = `${fieldName} is required`;
3358
4376
  }
3359
4377
  }
3360
- } else if (t7.isIdentifier(test.argument)) {
4378
+ } else if (t8.isIdentifier(test.argument)) {
3361
4379
  fieldName = test.argument.name;
3362
4380
  description = `${fieldName} is required`;
3363
4381
  }
3364
4382
  }
3365
- if (t7.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
3366
- if (t7.isMemberExpression(test.left)) {
4383
+ if (t8.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
4384
+ if (t8.isMemberExpression(test.left)) {
3367
4385
  const memberExpr = test.left;
3368
- if (t7.isIdentifier(memberExpr.object)) {
4386
+ if (t8.isIdentifier(memberExpr.object)) {
3369
4387
  fieldName = memberExpr.object.name;
3370
- if (t7.isNumericLiteral(test.right)) {
4388
+ if (t8.isNumericLiteral(test.right)) {
3371
4389
  description = `${fieldName} must be at least ${test.right.value} characters`;
3372
4390
  }
3373
4391
  }
3374
4392
  }
3375
4393
  }
3376
- if (t7.isUnaryExpression(test) && test.operator === "!") {
3377
- if (t7.isCallExpression(test.argument) && t7.isMemberExpression(test.argument.callee)) {
3378
- const methodName = t7.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
3379
- if (methodName === "includes" && t7.isIdentifier(test.argument.callee.object)) {
4394
+ if (t8.isUnaryExpression(test) && test.operator === "!") {
4395
+ if (t8.isCallExpression(test.argument) && t8.isMemberExpression(test.argument.callee)) {
4396
+ const methodName = t8.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
4397
+ if (methodName === "includes" && t8.isIdentifier(test.argument.callee.object)) {
3380
4398
  fieldName = test.argument.callee.object.name;
3381
4399
  description = `${fieldName} format is invalid`;
3382
4400
  }
@@ -3386,7 +4404,7 @@ var FormAnalyzer = class {
3386
4404
  }
3387
4405
  buildForms(filePath, validationRules) {
3388
4406
  if (this.inputElements.length === 0) return [];
3389
- const fileName = path3.basename(filePath, path3.extname(filePath));
4407
+ const fileName = path5.basename(filePath, path5.extname(filePath));
3390
4408
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
3391
4409
  const fields = this.inputElements.map((input) => {
3392
4410
  const fieldType = this.inferFieldType(input);
@@ -3436,14 +4454,104 @@ var FormAnalyzer = class {
3436
4454
  }
3437
4455
  };
3438
4456
 
4457
+ // src/pipeline/composition.ts
4458
+ var MAX_DEPTH = 3;
4459
+ var MAX_VISITED_PER_SCREEN = 60;
4460
+ var MAX_FAN_IN = 3;
4461
+ var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
4462
+ function descendants(graph, from, screenFiles) {
4463
+ const seen = /* @__PURE__ */ new Set([from]);
4464
+ const out = [];
4465
+ let frontier = [from];
4466
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
4467
+ const next = [];
4468
+ for (const file2 of frontier) {
4469
+ for (const child of graph.renderedComponentFiles(file2)) {
4470
+ if (seen.has(child)) continue;
4471
+ seen.add(child);
4472
+ if (screenFiles.has(child)) continue;
4473
+ out.push(child);
4474
+ next.push(child);
4475
+ if (out.length >= MAX_VISITED_PER_SCREEN) return out;
4476
+ }
4477
+ }
4478
+ frontier = next;
4479
+ }
4480
+ return out;
4481
+ }
4482
+ function addressable(locator) {
4483
+ if (!locator || typeof locator !== "object") return false;
4484
+ const l = locator;
4485
+ if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
4486
+ return typeof l["testID"] === "string" && l["testID"].length > 0;
4487
+ }
4488
+ async function composeAffordances(options) {
4489
+ const { screens, graph, analyzeFile, screenFiles } = options;
4490
+ const byScreen = /* @__PURE__ */ new Map();
4491
+ const fanIn = /* @__PURE__ */ new Map();
4492
+ for (const screen of screens) {
4493
+ if (!screen.filePath) continue;
4494
+ const children = descendants(graph, screen.filePath, screenFiles);
4495
+ byScreen.set(screen, children);
4496
+ for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
4497
+ }
4498
+ const analyzed = /* @__PURE__ */ new Map();
4499
+ let screensEnriched = 0;
4500
+ let filesMerged = 0;
4501
+ for (const [screen, children] of byScreen) {
4502
+ const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
4503
+ if (exclusive.length === 0) continue;
4504
+ const actionIds = new Set(screen.actions.map((a) => a.id));
4505
+ const targetIds = new Set((screen.targets ?? []).map((t16) => t16.id));
4506
+ const formIds = new Set(screen.forms.map((f) => f.id));
4507
+ let gained = false;
4508
+ for (const file2 of exclusive) {
4509
+ if (!analyzed.has(file2)) {
4510
+ try {
4511
+ analyzed.set(file2, await analyzeFile(file2));
4512
+ } catch {
4513
+ analyzed.set(file2, null);
4514
+ }
4515
+ }
4516
+ const child = analyzed.get(file2);
4517
+ if (!child) continue;
4518
+ let merged = false;
4519
+ for (const action of child.actions) {
4520
+ if (!addressable(action.locator)) continue;
4521
+ if (actionIds.has(action.id)) continue;
4522
+ actionIds.add(action.id);
4523
+ screen.actions.push(action);
4524
+ merged = true;
4525
+ }
4526
+ for (const target of child.targets ?? []) {
4527
+ if (!addressable(target.locator)) continue;
4528
+ if (targetIds.has(target.id)) continue;
4529
+ targetIds.add(target.id);
4530
+ (screen.targets ??= []).push(target);
4531
+ merged = true;
4532
+ }
4533
+ for (const form of child.forms) {
4534
+ if (form.fields.length === 0) continue;
4535
+ const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
4536
+ if (formIds.has(id)) continue;
4537
+ formIds.add(id);
4538
+ screen.forms.push({ ...form, id });
4539
+ merged = true;
4540
+ }
4541
+ if (merged) {
4542
+ filesMerged++;
4543
+ gained = true;
4544
+ }
4545
+ }
4546
+ if (gained) screensEnriched++;
4547
+ }
4548
+ return { screens, screensEnriched, filesMerged };
4549
+ }
4550
+
3439
4551
  // src/analyzers/ReactNativePlatformAnalyzer.ts
3440
4552
  var ReactNativePlatformAnalyzer = class {
3441
4553
  platform = "react-native";
3442
4554
  async analyze(config, options) {
3443
- const screenAnalyzer = new ScreenAnalyzer(config, {
3444
- strictScreens: options.strictScreens ?? true,
3445
- screenPatterns: options.screenPatterns
3446
- });
3447
4555
  const navigationAnalyzer = new NavigationAnalyzer(config, {
3448
4556
  navigationInclude: options.navigationInclude,
3449
4557
  navigationExclude: options.navigationExclude
@@ -3451,14 +4559,31 @@ var ReactNativePlatformAnalyzer = class {
3451
4559
  const componentAnalyzer = new ComponentAnalyzer(config);
3452
4560
  const formAnalyzer = new FormAnalyzer(config);
3453
4561
  console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
3454
- const [screens, navigation] = await Promise.all([
3455
- screenAnalyzer.analyze(),
3456
- navigationAnalyzer.analyze()
3457
- ]);
4562
+ const navigation = await navigationAnalyzer.analyze();
4563
+ const screenAnalyzer = new ScreenAnalyzer(config, {
4564
+ strictScreens: options.strictScreens ?? true,
4565
+ screenPatterns: options.screenPatterns,
4566
+ // Menos os arquivos que declaram navegador: ver `navigatorFiles`.
4567
+ routeTargetFiles: new Set(
4568
+ [...navigationAnalyzer.routeTargets.values()].filter(
4569
+ (file2) => !navigationAnalyzer.navigatorFiles.has(file2)
4570
+ )
4571
+ )
4572
+ });
4573
+ const screens = await screenAnalyzer.analyze();
4574
+ const composed = await composeAffordances({
4575
+ screens,
4576
+ graph: navigationAnalyzer.graph,
4577
+ analyzeFile: (file2) => screenAnalyzer.analyzeFile(file2),
4578
+ screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
4579
+ });
4580
+ console.log(
4581
+ `[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
4582
+ );
3458
4583
  console.log(
3459
4584
  `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
3460
4585
  );
3461
- const screenFiles = await (0, import_fast_glob3.default)(config.include || ["**/*.tsx", "**/*.ts"], {
4586
+ const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
3462
4587
  cwd: config.rootDir,
3463
4588
  ignore: config.exclude || ["**/node_modules/**"]
3464
4589
  });
@@ -3466,7 +4591,7 @@ var ReactNativePlatformAnalyzer = class {
3466
4591
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
3467
4592
  );
3468
4593
  const enrichmentPromises = screenFiles.map(async (file2) => {
3469
- const filePath = import_node_path.default.resolve(config.rootDir, file2);
4594
+ const filePath = import_node_path3.default.resolve(config.rootDir, file2);
3470
4595
  try {
3471
4596
  const [components, forms] = await Promise.all([
3472
4597
  componentAnalyzer.analyzeFile(filePath),
@@ -3521,7 +4646,8 @@ var ReactNativePlatformAnalyzer = class {
3521
4646
  screens: enrichedScreens,
3522
4647
  navigation,
3523
4648
  analyzedFiles: screenFiles.length,
3524
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
4649
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
4650
+ diagnostics: { navigation: navigationAnalyzer.diagnostics }
3525
4651
  };
3526
4652
  }
3527
4653
  mergeForm(target, source) {
@@ -3539,9 +4665,12 @@ var ReactNativePlatformAnalyzer = class {
3539
4665
  findEquivalentField(fields, incoming) {
3540
4666
  return fields.find((field) => {
3541
4667
  if (field.name && incoming.name && field.name === incoming.name) return true;
3542
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
3543
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
3544
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
4668
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
4669
+ return true;
4670
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
4671
+ return true;
4672
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
4673
+ return true;
3545
4674
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
3546
4675
  return true;
3547
4676
  }
@@ -3574,7 +4703,9 @@ var ReactNativePlatformAnalyzer = class {
3574
4703
  if (incoming.fields.length === 0) return void 0;
3575
4704
  let best;
3576
4705
  for (const form of forms) {
3577
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
4706
+ const overlap = incoming.fields.filter(
4707
+ (field) => this.findEquivalentField(form.fields, field)
4708
+ ).length;
3578
4709
  if (overlap > 0 && (!best || overlap > best.overlap)) {
3579
4710
  best = { form, overlap };
3580
4711
  }
@@ -3601,9 +4732,8 @@ var GenericPlatformAnalyzer = class {
3601
4732
  // src/analyzers/web/WebScreenAnalyzer.ts
3602
4733
  var import_promises2 = __toESM(require("fs/promises"));
3603
4734
  var import_path5 = __toESM(require("path"));
3604
- var import_fast_glob4 = __toESM(require("fast-glob"));
3605
4735
  var import_traverse9 = __toESM(require("@babel/traverse"));
3606
- var t10 = __toESM(require("@babel/types"));
4736
+ var t11 = __toESM(require("@babel/types"));
3607
4737
 
3608
4738
  // src/ast/jsx/web/classify.ts
3609
4739
  var VIEW_TAGS = /* @__PURE__ */ new Set([
@@ -3719,16 +4849,16 @@ function classifyWebJsxComponent(name, element) {
3719
4849
  }
3720
4850
 
3721
4851
  // src/ast/jsx/names.ts
3722
- var t8 = __toESM(require("@babel/types"));
4852
+ var t9 = __toESM(require("@babel/types"));
3723
4853
  function getJsxElementName(openingElement) {
3724
4854
  return getJsxName(openingElement.name);
3725
4855
  }
3726
4856
  function getJsxName(name) {
3727
- if (t8.isJSXIdentifier(name)) return name.name;
3728
- if (t8.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
3729
- if (t8.isJSXMemberExpression(name)) {
4857
+ if (t9.isJSXIdentifier(name)) return name.name;
4858
+ if (t9.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
4859
+ if (t9.isJSXMemberExpression(name)) {
3730
4860
  const objectName = getJsxName(name.object);
3731
- const propertyName = t8.isJSXIdentifier(name.property) ? name.property.name : null;
4861
+ const propertyName = t9.isJSXIdentifier(name.property) ? name.property.name : null;
3732
4862
  return objectName && propertyName ? `${objectName}.${propertyName}` : null;
3733
4863
  }
3734
4864
  return null;
@@ -3736,32 +4866,32 @@ function getJsxName(name) {
3736
4866
 
3737
4867
  // src/ast/navigation/web-calls.ts
3738
4868
  var import_traverse8 = __toESM(require("@babel/traverse"));
3739
- var t9 = __toESM(require("@babel/types"));
4869
+ var t10 = __toESM(require("@babel/types"));
3740
4870
  var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
3741
4871
  function staticRoutePath(node) {
3742
4872
  if (!node) return void 0;
3743
- if (t9.isStringLiteral(node)) return node.value;
3744
- if (t9.isTemplateLiteral(node)) {
3745
- let path11 = "";
4873
+ if (t10.isStringLiteral(node)) return node.value;
4874
+ if (t10.isTemplateLiteral(node)) {
4875
+ let path13 = "";
3746
4876
  node.quasis.forEach((quasi, index) => {
3747
- path11 += quasi.value.cooked ?? quasi.value.raw;
4877
+ path13 += quasi.value.cooked ?? quasi.value.raw;
3748
4878
  const expr = node.expressions[index];
3749
- if (expr) path11 += `:${paramNameOf(expr)}`;
4879
+ if (expr) path13 += `:${paramNameOf(expr)}`;
3750
4880
  });
3751
- return path11;
4881
+ return path13;
3752
4882
  }
3753
4883
  return void 0;
3754
4884
  }
3755
4885
  function paramNameOf(expr) {
3756
- if (t9.isIdentifier(expr)) return expr.name;
3757
- if (t9.isMemberExpression(expr) && t9.isIdentifier(expr.property)) return expr.property.name;
4886
+ if (t10.isIdentifier(expr)) return expr.name;
4887
+ if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.property)) return expr.property.name;
3758
4888
  return "param";
3759
4889
  }
3760
4890
  function extractWebNavigationCalls(ast) {
3761
4891
  const calls = [];
3762
4892
  const inspect = (node) => {
3763
- if (t9.isCallExpression(node)) {
3764
- if (t9.isIdentifier(node.callee) && node.callee.name === "navigate") {
4893
+ if (t10.isCallExpression(node)) {
4894
+ if (t10.isIdentifier(node.callee) && node.callee.name === "navigate") {
3765
4895
  const first = node.arguments[0];
3766
4896
  const targetPath = staticRoutePath(first);
3767
4897
  if (targetPath !== void 0) {
@@ -3769,12 +4899,12 @@ function extractWebNavigationCalls(ast) {
3769
4899
  method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
3770
4900
  targetPath
3771
4901
  });
3772
- } else if (t9.isNumericLiteral(first) || t9.isUnaryExpression(first) && first.operator === "-") {
4902
+ } else if (t10.isNumericLiteral(first) || t10.isUnaryExpression(first) && first.operator === "-") {
3773
4903
  calls.push({ method: "goBack" });
3774
4904
  }
3775
4905
  return;
3776
4906
  }
3777
- if (t9.isMemberExpression(node.callee) && t9.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t9.isIdentifier(node.callee.property)) {
4907
+ if (t10.isMemberExpression(node.callee) && t10.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t10.isIdentifier(node.callee.property)) {
3778
4908
  const method = node.callee.property.name;
3779
4909
  const targetPath = staticRoutePath(node.arguments[0]);
3780
4910
  if ((method === "push" || method === "navigate") && targetPath !== void 0) {
@@ -3787,25 +4917,25 @@ function extractWebNavigationCalls(ast) {
3787
4917
  }
3788
4918
  return;
3789
4919
  }
3790
- if (t9.isAssignmentExpression(node) && t9.isMemberExpression(node.left) && t9.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && t9.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
4920
+ if (t10.isAssignmentExpression(node) && t10.isMemberExpression(node.left) && t10.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && t10.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
3791
4921
  calls.push({ method: "navigate", targetPath: node.right.value });
3792
4922
  }
3793
4923
  };
3794
4924
  (0, import_traverse8.default)(ast, {
3795
- noScope: !t9.isFile(ast),
4925
+ noScope: !t10.isFile(ast),
3796
4926
  enter: (nodePath) => inspect(nodePath.node)
3797
4927
  });
3798
4928
  return calls;
3799
4929
  }
3800
4930
  function hasReplaceOption(arg) {
3801
- if (!arg || !t9.isObjectExpression(arg)) return false;
4931
+ if (!arg || !t10.isObjectExpression(arg)) return false;
3802
4932
  return arg.properties.some(
3803
- (prop) => t9.isObjectProperty(prop) && t9.isIdentifier(prop.key) && prop.key.name === "replace" && t9.isBooleanLiteral(prop.value) && prop.value.value === true
4933
+ (prop) => t10.isObjectProperty(prop) && t10.isIdentifier(prop.key) && prop.key.name === "replace" && t10.isBooleanLiteral(prop.value) && prop.value.value === true
3804
4934
  );
3805
4935
  }
3806
4936
  function isLocationExpression(node) {
3807
- if (t9.isIdentifier(node)) return node.name === "location";
3808
- return t9.isMemberExpression(node) && t9.isIdentifier(node.object) && node.object.name === "window" && t9.isIdentifier(node.property) && node.property.name === "location";
4937
+ if (t10.isIdentifier(node)) return node.name === "location";
4938
+ return t10.isMemberExpression(node) && t10.isIdentifier(node.object) && node.object.name === "window" && t10.isIdentifier(node.property) && node.property.name === "location";
3809
4939
  }
3810
4940
 
3811
4941
  // src/analyzers/web/WebScreenAnalyzer.ts
@@ -3833,10 +4963,10 @@ var WebScreenAnalyzer = class {
3833
4963
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
3834
4964
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
3835
4965
  } = this.config;
3836
- const files = (await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
4966
+ const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
3837
4967
  (file2) => !file2.endsWith(".d.ts")
3838
4968
  );
3839
- const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
4969
+ const patternMatches = await globSorted(this.screenPatterns, {
3840
4970
  cwd: this.config.rootDir,
3841
4971
  ignore: exclude
3842
4972
  });
@@ -3930,7 +5060,7 @@ var WebScreenAnalyzer = class {
3930
5060
  CallExpression: (nodePath) => {
3931
5061
  if (!isRegisterScreenCallee(nodePath.node.callee)) return;
3932
5062
  const arg = nodePath.node.arguments[0];
3933
- if (t10.isObjectExpression(arg)) {
5063
+ if (t11.isObjectExpression(arg)) {
3934
5064
  plain = literalToPlain(arg);
3935
5065
  }
3936
5066
  }
@@ -3977,20 +5107,20 @@ var WebScreenAnalyzer = class {
3977
5107
  (0, import_traverse9.default)(ast, {
3978
5108
  ExportDefaultDeclaration: (nodePath) => {
3979
5109
  const declaration = nodePath.node.declaration;
3980
- if (t10.isFunctionDeclaration(declaration) && declaration.id?.name) {
5110
+ if (t11.isFunctionDeclaration(declaration) && declaration.id?.name) {
3981
5111
  defaultName = declaration.id.name;
3982
- } else if (t10.isIdentifier(declaration)) {
5112
+ } else if (t11.isIdentifier(declaration)) {
3983
5113
  defaultName = declaration.name;
3984
5114
  }
3985
5115
  },
3986
5116
  ExportNamedDeclaration: (nodePath) => {
3987
5117
  if (firstExported) return;
3988
5118
  const declaration = nodePath.node.declaration;
3989
- if (t10.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
5119
+ if (t11.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
3990
5120
  firstExported = declaration.id.name;
3991
- } else if (t10.isVariableDeclaration(declaration)) {
5121
+ } else if (t11.isVariableDeclaration(declaration)) {
3992
5122
  for (const declarator of declaration.declarations) {
3993
- if (t10.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t10.isArrowFunctionExpression(declarator.init) || t10.isFunctionExpression(declarator.init))) {
5123
+ if (t11.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t11.isArrowFunctionExpression(declarator.init) || t11.isFunctionExpression(declarator.init))) {
3994
5124
  firstExported = declarator.id.name;
3995
5125
  break;
3996
5126
  }
@@ -4162,15 +5292,15 @@ var WebScreenAnalyzer = class {
4162
5292
  });
4163
5293
  }
4164
5294
  for (const attr of opening.attributes) {
4165
- if (!t10.isJSXAttribute(attr) || !t10.isJSXIdentifier(attr.name)) continue;
5295
+ if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
4166
5296
  const attrName = attr.name.name;
4167
5297
  if (attrName === "required") {
4168
5298
  if (attr.value === null) field.required = true;
4169
- else if (t10.isJSXExpressionContainer(attr.value) && t10.isBooleanLiteral(attr.value.expression)) {
5299
+ else if (t11.isJSXExpressionContainer(attr.value) && t11.isBooleanLiteral(attr.value.expression)) {
4170
5300
  field.required = attr.value.expression.value;
4171
5301
  }
4172
5302
  }
4173
- if ((attrName === "value" || attrName === "checked") && attr.value && t10.isJSXExpressionContainer(attr.value) && t10.isIdentifier(attr.value.expression)) {
5303
+ if ((attrName === "value" || attrName === "checked") && attr.value && t11.isJSXExpressionContainer(attr.value) && t11.isIdentifier(attr.value.expression)) {
4174
5304
  field.valueBinding = attr.value.expression.name;
4175
5305
  if (!field.name) field.name = attr.value.expression.name;
4176
5306
  }
@@ -4220,7 +5350,7 @@ var WebScreenAnalyzer = class {
4220
5350
  extractSelectOptions(selectElement) {
4221
5351
  const options = [];
4222
5352
  for (const child of selectElement.children) {
4223
- if (!t10.isJSXElement(child)) continue;
5353
+ if (!t11.isJSXElement(child)) continue;
4224
5354
  if (getJsxElementName(child.openingElement) !== "option") continue;
4225
5355
  const value = getStringAttr(child.openingElement, "value");
4226
5356
  const label = jsxTextContent(child) || value || "";
@@ -4331,14 +5461,14 @@ var WebScreenAnalyzer = class {
4331
5461
  }
4332
5462
  let handlerName;
4333
5463
  const onClickAttr = opening.attributes.find(
4334
- (attr) => t10.isJSXAttribute(attr) && t10.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
5464
+ (attr) => t11.isJSXAttribute(attr) && t11.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
4335
5465
  );
4336
- if (onClickAttr?.value && t10.isJSXExpressionContainer(onClickAttr.value)) {
5466
+ if (onClickAttr?.value && t11.isJSXExpressionContainer(onClickAttr.value)) {
4337
5467
  const expr = onClickAttr.value.expression;
4338
- if (t10.isIdentifier(expr)) {
5468
+ if (t11.isIdentifier(expr)) {
4339
5469
  handlerName = expr.name;
4340
5470
  action.handler = expr.name;
4341
- } else if (!t10.isJSXEmptyExpression(expr)) {
5471
+ } else if (!t11.isJSXEmptyExpression(expr)) {
4342
5472
  const inlineNavCalls = extractWebNavigationCalls(expr);
4343
5473
  const inlineNav = inlineNavCalls.find((call) => call.targetPath);
4344
5474
  if (inlineNav?.targetPath) {
@@ -4477,12 +5607,12 @@ var WebScreenAnalyzer = class {
4477
5607
  }
4478
5608
  handlerNameFromAttr(opening, attrName) {
4479
5609
  const attr = opening.attributes.find(
4480
- (candidate) => t10.isJSXAttribute(candidate) && t10.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
5610
+ (candidate) => t11.isJSXAttribute(candidate) && t11.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
4481
5611
  );
4482
- if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
5612
+ if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
4483
5613
  const expr = attr.value.expression;
4484
- if (t10.isIdentifier(expr)) return expr.name;
4485
- if (t10.isArrowFunctionExpression(expr) || t10.isFunctionExpression(expr)) {
5614
+ if (t11.isIdentifier(expr)) return expr.name;
5615
+ if (t11.isArrowFunctionExpression(expr) || t11.isFunctionExpression(expr)) {
4486
5616
  return firstCalledFunctionName(expr) ?? "anonymous";
4487
5617
  }
4488
5618
  return void 0;
@@ -4516,11 +5646,11 @@ var WebScreenAnalyzer = class {
4516
5646
  (0, import_traverse9.default)(ast, {
4517
5647
  JSXExpressionContainer: (nodePath) => {
4518
5648
  const expr = nodePath.node.expression;
4519
- if (!t10.isCallExpression(expr) || !t10.isMemberExpression(expr.callee) || !t10.isIdentifier(expr.callee.object) || !t10.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
5649
+ if (!t11.isCallExpression(expr) || !t11.isMemberExpression(expr.callee) || !t11.isIdentifier(expr.callee.object) || !t11.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
4520
5650
  return;
4521
5651
  }
4522
5652
  const callback = expr.arguments[0];
4523
- if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
5653
+ if (!t11.isArrowFunctionExpression(callback) && !t11.isFunctionExpression(callback)) return;
4524
5654
  const enclosing = nodePath.findParent(
4525
5655
  (p) => p.isJSXElement()
4526
5656
  );
@@ -4555,22 +5685,22 @@ var WebScreenAnalyzer = class {
4555
5685
  }
4556
5686
  };
4557
5687
  function isRegisterScreenCallee(callee) {
4558
- return t10.isIdentifier(callee) && callee.name === "registerScreen" || t10.isMemberExpression(callee) && t10.isIdentifier(callee.property) && callee.property.name === "registerScreen";
5688
+ return t11.isIdentifier(callee) && callee.name === "registerScreen" || t11.isMemberExpression(callee) && t11.isIdentifier(callee.property) && callee.property.name === "registerScreen";
4559
5689
  }
4560
5690
  function literalToPlain(node) {
4561
- if (t10.isStringLiteral(node) || t10.isNumericLiteral(node) || t10.isBooleanLiteral(node)) {
5691
+ if (t11.isStringLiteral(node) || t11.isNumericLiteral(node) || t11.isBooleanLiteral(node)) {
4562
5692
  return node.value;
4563
5693
  }
4564
- if (t10.isNullLiteral(node)) return null;
4565
- if (t10.isArrayExpression(node)) {
4566
- return node.elements.filter((el) => el !== null && t10.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
5694
+ if (t11.isNullLiteral(node)) return null;
5695
+ if (t11.isArrayExpression(node)) {
5696
+ return node.elements.filter((el) => el !== null && t11.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
4567
5697
  }
4568
- if (t10.isObjectExpression(node)) {
5698
+ if (t11.isObjectExpression(node)) {
4569
5699
  const out = {};
4570
5700
  for (const prop of node.properties) {
4571
- if (!t10.isObjectProperty(prop)) continue;
4572
- const key = t10.isIdentifier(prop.key) ? prop.key.name : t10.isStringLiteral(prop.key) ? prop.key.value : void 0;
4573
- if (!key || !t10.isExpression(prop.value)) continue;
5701
+ if (!t11.isObjectProperty(prop)) continue;
5702
+ const key = t11.isIdentifier(prop.key) ? prop.key.name : t11.isStringLiteral(prop.key) ? prop.key.value : void 0;
5703
+ if (!key || !t11.isExpression(prop.value)) continue;
4574
5704
  const value = literalToPlain(prop.value);
4575
5705
  if (value !== void 0) out[key] = value;
4576
5706
  }
@@ -4582,12 +5712,12 @@ function jsxTextContent(element) {
4582
5712
  const parts = [];
4583
5713
  const walk = (children) => {
4584
5714
  for (const child of children) {
4585
- if (t10.isJSXText(child)) {
5715
+ if (t11.isJSXText(child)) {
4586
5716
  const trimmed = child.value.replace(/\s+/g, " ").trim();
4587
5717
  if (trimmed) parts.push(trimmed);
4588
- } else if (t10.isJSXExpressionContainer(child) && t10.isStringLiteral(child.expression)) {
5718
+ } else if (t11.isJSXExpressionContainer(child) && t11.isStringLiteral(child.expression)) {
4589
5719
  parts.push(child.expression.value);
4590
- } else if (t10.isJSXElement(child)) {
5720
+ } else if (t11.isJSXElement(child)) {
4591
5721
  walk(child.children);
4592
5722
  }
4593
5723
  }
@@ -4597,26 +5727,26 @@ function jsxTextContent(element) {
4597
5727
  return text.length > 0 ? text : void 0;
4598
5728
  }
4599
5729
  function firstCalledFunctionName(node) {
4600
- if (t10.isIdentifier(node)) return node.name;
4601
- if (t10.isArrowFunctionExpression(node) || t10.isFunctionExpression(node)) {
5730
+ if (t11.isIdentifier(node)) return node.name;
5731
+ if (t11.isArrowFunctionExpression(node) || t11.isFunctionExpression(node)) {
4602
5732
  return firstCalledFunctionName(node.body);
4603
5733
  }
4604
- if (t10.isBlockStatement(node)) {
5734
+ if (t11.isBlockStatement(node)) {
4605
5735
  for (const statement of node.body) {
4606
5736
  const handler = firstCalledFunctionName(statement);
4607
5737
  if (handler) return handler;
4608
5738
  }
4609
5739
  return void 0;
4610
5740
  }
4611
- if (t10.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
4612
- if (t10.isReturnStatement(node)) {
5741
+ if (t11.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
5742
+ if (t11.isReturnStatement(node)) {
4613
5743
  return node.argument ? firstCalledFunctionName(node.argument) : void 0;
4614
5744
  }
4615
- if (t10.isAwaitExpression(node) || t10.isUnaryExpression(node)) {
5745
+ if (t11.isAwaitExpression(node) || t11.isUnaryExpression(node)) {
4616
5746
  return firstCalledFunctionName(node.argument);
4617
5747
  }
4618
- if (t10.isCallExpression(node)) {
4619
- if (t10.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
5748
+ if (t11.isCallExpression(node)) {
5749
+ if (t11.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
4620
5750
  return node.callee.name;
4621
5751
  }
4622
5752
  return void 0;
@@ -4631,8 +5761,8 @@ function containsWindowConfirm(body) {
4631
5761
  noScope: true,
4632
5762
  CallExpression: (nodePath) => {
4633
5763
  const callee = nodePath.node.callee;
4634
- if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
4635
- if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
5764
+ if (t11.isIdentifier(callee) && callee.name === "confirm") found = true;
5765
+ if (t11.isMemberExpression(callee) && t11.isIdentifier(callee.object) && callee.object.name === "window" && t11.isIdentifier(callee.property) && callee.property.name === "confirm") {
4636
5766
  found = true;
4637
5767
  }
4638
5768
  }
@@ -4647,9 +5777,9 @@ function routePathAttr(opening, attrName) {
4647
5777
  const literal = getStringAttr(opening, attrName);
4648
5778
  if (literal) return literal;
4649
5779
  const attr = opening.attributes.find(
4650
- (candidate) => t10.isJSXAttribute(candidate) && t10.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
5780
+ (candidate) => t11.isJSXAttribute(candidate) && t11.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
4651
5781
  );
4652
- if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
5782
+ if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
4653
5783
  return staticRoutePath(attr.value.expression);
4654
5784
  }
4655
5785
  function slugify(label) {
@@ -4664,10 +5794,10 @@ function isWeakInferredFieldName(name) {
4664
5794
  function collectionItemNames(callback) {
4665
5795
  const names = /* @__PURE__ */ new Set(["item"]);
4666
5796
  const firstParam = callback.params[0];
4667
- if (t10.isIdentifier(firstParam)) names.add(firstParam.name);
4668
- if (t10.isObjectPattern(firstParam)) {
5797
+ if (t11.isIdentifier(firstParam)) names.add(firstParam.name);
5798
+ if (t11.isObjectPattern(firstParam)) {
4669
5799
  for (const prop of firstParam.properties) {
4670
- if (t10.isObjectProperty(prop) && t10.isIdentifier(prop.key) && t10.isIdentifier(prop.value)) {
5800
+ if (t11.isObjectProperty(prop) && t11.isIdentifier(prop.key) && t11.isIdentifier(prop.value)) {
4671
5801
  names.add(prop.value.name);
4672
5802
  }
4673
5803
  }
@@ -4683,7 +5813,7 @@ function collectionDisplayFields(callback, itemNames) {
4683
5813
  noScope: true,
4684
5814
  MemberExpression: (nodePath) => {
4685
5815
  const node = nodePath.node;
4686
- if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
5816
+ if (t11.isIdentifier(node.object) && itemNames.has(node.object.name) && t11.isIdentifier(node.property)) {
4687
5817
  fields.add(node.property.name);
4688
5818
  }
4689
5819
  }
@@ -4700,10 +5830,10 @@ function collectionKeyField(callback, itemNames) {
4700
5830
  noScope: true,
4701
5831
  JSXAttribute: (nodePath) => {
4702
5832
  const attr = nodePath.node;
4703
- if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
4704
- if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
5833
+ if (!t11.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
5834
+ if (!attr.value || !t11.isJSXExpressionContainer(attr.value)) return;
4705
5835
  const expr = attr.value.expression;
4706
- if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
5836
+ if (t11.isMemberExpression(expr) && t11.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t11.isIdentifier(expr.property)) {
4707
5837
  keyField = keyField ?? expr.property.name;
4708
5838
  }
4709
5839
  }
@@ -4726,18 +5856,18 @@ function callbackReturnsTag(callback, tags) {
4726
5856
  let found = false;
4727
5857
  const inspect = (node) => {
4728
5858
  if (!node || found) return;
4729
- if (t10.isJSXElement(node)) {
5859
+ if (t11.isJSXElement(node)) {
4730
5860
  const name = getJsxElementName(node.openingElement);
4731
5861
  if (name && tags.has(name)) found = true;
4732
5862
  return;
4733
5863
  }
4734
- if (t10.isBlockStatement(node)) {
5864
+ if (t11.isBlockStatement(node)) {
4735
5865
  for (const statement of node.body) {
4736
- if (t10.isReturnStatement(statement)) inspect(statement.argument);
5866
+ if (t11.isReturnStatement(statement)) inspect(statement.argument);
4737
5867
  }
4738
5868
  }
4739
- if (t10.isParenthesizedExpression(node)) inspect(node.expression);
4740
- if (t10.isConditionalExpression(node)) {
5869
+ if (t11.isParenthesizedExpression(node)) inspect(node.expression);
5870
+ if (t11.isConditionalExpression(node)) {
4741
5871
  inspect(node.consequent);
4742
5872
  inspect(node.alternate);
4743
5873
  }
@@ -4764,9 +5894,8 @@ function inferIdentityFields(keyField, displayFields) {
4764
5894
  // src/analyzers/web/WebNavigationAnalyzer.ts
4765
5895
  var import_fs7 = require("fs");
4766
5896
  var import_path6 = __toESM(require("path"));
4767
- var import_fast_glob5 = __toESM(require("fast-glob"));
4768
5897
  var import_traverse10 = __toESM(require("@babel/traverse"));
4769
- var t11 = __toESM(require("@babel/types"));
5898
+ var t12 = __toESM(require("@babel/types"));
4770
5899
  var WEB_NAVIGATOR_TYPE = "route";
4771
5900
  var WebNavigationAnalyzer = class {
4772
5901
  config;
@@ -4783,7 +5912,9 @@ var WebNavigationAnalyzer = class {
4783
5912
  for (const filePath of files) {
4784
5913
  try {
4785
5914
  const content = await import_fs7.promises.readFile(filePath, "utf-8");
4786
- if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
5915
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
5916
+ content
5917
+ )) {
4787
5918
  continue;
4788
5919
  }
4789
5920
  const ast = parseSource(content, this.config.parserPlugins);
@@ -4813,7 +5944,7 @@ var WebNavigationAnalyzer = class {
4813
5944
  ...this.config.exclude || [],
4814
5945
  ...this.navigationExclude
4815
5946
  ];
4816
- const files = await (0, import_fast_glob5.default)(patterns, { cwd: this.config.rootDir, ignore });
5947
+ const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
4817
5948
  return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
4818
5949
  }
4819
5950
  // ── JSX <Route> style ────────────────────────────────────────────
@@ -4824,7 +5955,7 @@ var WebNavigationAnalyzer = class {
4824
5955
  const name = getJsxElementName(opening);
4825
5956
  if (name !== "Route") {
4826
5957
  for (const child of element.children) {
4827
- if (t11.isJSXElement(child)) visitRoute(child, parentPath);
5958
+ if (t12.isJSXElement(child)) visitRoute(child, parentPath);
4828
5959
  }
4829
5960
  return;
4830
5961
  }
@@ -4833,13 +5964,13 @@ var WebNavigationAnalyzer = class {
4833
5964
  const fullPath = this.joinPaths(parentPath, segment, isIndex);
4834
5965
  const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
4835
5966
  const isLeaf = !element.children.some(
4836
- (child) => t11.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
5967
+ (child) => t12.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
4837
5968
  );
4838
5969
  if ((segment || isIndex) && (componentName || isLeaf)) {
4839
5970
  routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
4840
5971
  }
4841
5972
  for (const child of element.children) {
4842
- if (t11.isJSXElement(child)) visitRoute(child, fullPath);
5973
+ if (t12.isJSXElement(child)) visitRoute(child, fullPath);
4843
5974
  }
4844
5975
  };
4845
5976
  (0, import_traverse10.default)(ast, {
@@ -4861,13 +5992,13 @@ var WebNavigationAnalyzer = class {
4861
5992
  /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
4862
5993
  componentNameFromElementAttr(opening) {
4863
5994
  for (const attr of opening.attributes) {
4864
- if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
4865
- if (attr.name.name === "element" && t11.isJSXExpressionContainer(attr.value)) {
5995
+ if (!t12.isJSXAttribute(attr) || !t12.isJSXIdentifier(attr.name)) continue;
5996
+ if (attr.name.name === "element" && t12.isJSXExpressionContainer(attr.value)) {
4866
5997
  const expr = attr.value.expression;
4867
- if (t11.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
5998
+ if (t12.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
4868
5999
  }
4869
- if (attr.name.name === "Component" && t11.isJSXExpressionContainer(attr.value)) {
4870
- if (t11.isIdentifier(attr.value.expression)) return attr.value.expression.name;
6000
+ if (attr.name.name === "Component" && t12.isJSXExpressionContainer(attr.value)) {
6001
+ if (t12.isIdentifier(attr.value.expression)) return attr.value.expression.name;
4871
6002
  }
4872
6003
  }
4873
6004
  return null;
@@ -4884,9 +6015,9 @@ var WebNavigationAnalyzer = class {
4884
6015
  (0, import_traverse10.default)(ast, {
4885
6016
  CallExpression: (nodePath) => {
4886
6017
  const callee = nodePath.node.callee;
4887
- if (!t11.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
6018
+ if (!t12.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
4888
6019
  const first = nodePath.node.arguments[0];
4889
- if (!t11.isArrayExpression(first)) return;
6020
+ if (!t12.isArrayExpression(first)) return;
4890
6021
  this.visitRouteObjects(first, "", routes);
4891
6022
  }
4892
6023
  });
@@ -4894,21 +6025,21 @@ var WebNavigationAnalyzer = class {
4894
6025
  }
4895
6026
  visitRouteObjects(arr, parentPath, out) {
4896
6027
  for (const element of arr.elements) {
4897
- if (!t11.isObjectExpression(element)) continue;
6028
+ if (!t12.isObjectExpression(element)) continue;
4898
6029
  let segment;
4899
6030
  let isIndex = false;
4900
6031
  let componentName;
4901
6032
  let children;
4902
6033
  for (const prop of element.properties) {
4903
- if (!t11.isObjectProperty(prop) || !t11.isIdentifier(prop.key)) continue;
6034
+ if (!t12.isObjectProperty(prop) || !t12.isIdentifier(prop.key)) continue;
4904
6035
  const key = prop.key.name;
4905
- if (key === "path" && t11.isStringLiteral(prop.value)) segment = prop.value.value;
4906
- if (key === "index" && t11.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
4907
- if (key === "element" && t11.isJSXElement(prop.value)) {
6036
+ if (key === "path" && t12.isStringLiteral(prop.value)) segment = prop.value.value;
6037
+ if (key === "index" && t12.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
6038
+ if (key === "element" && t12.isJSXElement(prop.value)) {
4908
6039
  componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
4909
6040
  }
4910
- if (key === "Component" && t11.isIdentifier(prop.value)) componentName = prop.value.name;
4911
- if (key === "children" && t11.isArrayExpression(prop.value)) children = prop.value;
6041
+ if (key === "Component" && t12.isIdentifier(prop.value)) componentName = prop.value.name;
6042
+ if (key === "children" && t12.isArrayExpression(prop.value)) children = prop.value;
4912
6043
  }
4913
6044
  const fullPath = this.joinPaths(parentPath, segment, isIndex);
4914
6045
  if ((segment !== void 0 || isIndex) && (componentName || !children)) {
@@ -4986,11 +6117,13 @@ var WebNavigationAnalyzer = class {
4986
6117
  return {
4987
6118
  screens,
4988
6119
  initialScreen: initialRoute?.screenName ?? "",
4989
- navigators: routes.length > 0 ? [{
4990
- name: navigatorName,
4991
- type: WEB_NAVIGATOR_TYPE,
4992
- screens: screenNames
4993
- }] : []
6120
+ navigators: routes.length > 0 ? [
6121
+ {
6122
+ name: navigatorName,
6123
+ type: WEB_NAVIGATOR_TYPE,
6124
+ screens: screenNames
6125
+ }
6126
+ ] : []
4994
6127
  };
4995
6128
  }
4996
6129
  };
@@ -5109,7 +6242,7 @@ var ReactWebPlatformAnalyzer = class {
5109
6242
 
5110
6243
  // src/manifest/loadManifest.ts
5111
6244
  var import_promises3 = require("fs/promises");
5112
- var import_node_path2 = __toESM(require("path"));
6245
+ var import_node_path4 = __toESM(require("path"));
5113
6246
 
5114
6247
  // ../../node_modules/zod/v3/external.js
5115
6248
  var external_exports = {};
@@ -5316,8 +6449,8 @@ var ZodParsedType = util.arrayToEnum([
5316
6449
  "set"
5317
6450
  ]);
5318
6451
  var getParsedType = (data) => {
5319
- const t15 = typeof data;
5320
- switch (t15) {
6452
+ const t16 = typeof data;
6453
+ switch (t16) {
5321
6454
  case "undefined":
5322
6455
  return ZodParsedType.undefined;
5323
6456
  case "string":
@@ -5589,8 +6722,8 @@ function getErrorMap() {
5589
6722
 
5590
6723
  // ../../node_modules/zod/v3/helpers/parseUtil.js
5591
6724
  var makeIssue = (params) => {
5592
- const { data, path: path11, errorMaps, issueData } = params;
5593
- const fullPath = [...path11, ...issueData.path || []];
6725
+ const { data, path: path13, errorMaps, issueData } = params;
6726
+ const fullPath = [...path13, ...issueData.path || []];
5594
6727
  const fullIssue = {
5595
6728
  ...issueData,
5596
6729
  path: fullPath
@@ -5706,11 +6839,11 @@ var errorUtil;
5706
6839
 
5707
6840
  // ../../node_modules/zod/v3/types.js
5708
6841
  var ParseInputLazyPath = class {
5709
- constructor(parent, value, path11, key) {
6842
+ constructor(parent, value, path13, key) {
5710
6843
  this._cachedPath = [];
5711
6844
  this.parent = parent;
5712
6845
  this.data = value;
5713
- this._path = path11;
6846
+ this._path = path13;
5714
6847
  this._key = key;
5715
6848
  }
5716
6849
  get path() {
@@ -9152,7 +10285,18 @@ var coerce = {
9152
10285
  };
9153
10286
  var NEVER = INVALID;
9154
10287
 
9155
- // ../shared/dist/chunk-MKT45LRA.mjs
10288
+ // ../shared/dist/chunk-SWQOZWHP.mjs
10289
+ var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
10290
+ var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
10291
+ function isSafeImageUrl(value) {
10292
+ if (value === null || value === void 0) return true;
10293
+ const trimmed = value.trim();
10294
+ if (trimmed === "") return true;
10295
+ if (!HAS_SCHEME_RE.test(trimmed)) return true;
10296
+ return SAFE_IMAGE_URL_RE.test(trimmed);
10297
+ }
10298
+
10299
+ // ../shared/dist/chunk-NMQNNPSJ.mjs
9156
10300
  var locatorSourceSchema = external_exports.enum([
9157
10301
  "appilotsId",
9158
10302
  "testID",
@@ -9331,6 +10475,23 @@ var actionDescriptorSchema = external_exports.object({
9331
10475
  effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
9332
10476
  riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
9333
10477
  requiresConfirmation: external_exports.boolean().optional(),
10478
+ /**
10479
+ * How long this action's work is expected to take, in milliseconds,
10480
+ * DECLARED by the app — not inferred (that is `appilotsInferred`).
10481
+ *
10482
+ * The SDK's post-action wait was a constant: 6s, or 10s when the
10483
+ * generator could prove the handler awaits something. No constant
10484
+ * fits, because app operations run from ~100ms to minutes, and the
10485
+ * failure is silent in both directions — too short and the agent
10486
+ * photographs a loading screen with no controls on it, too long and
10487
+ * every fast press pays for the slowest one. Only the app knows.
10488
+ *
10489
+ * Absent means absent: the SDK keeps its current defaults, so a
10490
+ * document generated before this field existed behaves exactly as it
10491
+ * did. The SDK also clamps the value to its own safety ceiling — a
10492
+ * declared budget is a request, not a licence to hang the session.
10493
+ */
10494
+ asyncBudgetMs: external_exports.number().int().positive().optional(),
9334
10495
  appilotsInferred: appilotsInferredActionSchema.optional()
9335
10496
  }).passthrough();
9336
10497
  var screenPermissionDescriptorSchema = external_exports.object({
@@ -9513,7 +10674,7 @@ var uploadMCPDocumentSchema = external_exports.object({
9513
10674
  version: external_exports.string().default("1.0"),
9514
10675
  content: external_exports.record(external_exports.unknown())
9515
10676
  });
9516
- var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
10677
+ var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
9517
10678
  var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
9518
10679
  var createApiKeySchema = external_exports.object({
9519
10680
  name: external_exports.string().min(1).max(100),
@@ -9524,8 +10685,8 @@ var createApiKeySchema = external_exports.object({
9524
10685
  scope: apiKeyScopeSchema.default("sdk"),
9525
10686
  environment: apiKeyEnvironmentSchema.optional(),
9526
10687
  expiresAt: external_exports.string().datetime().optional()
9527
- }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
9528
- message: 'projectId is required for scope "sdk"',
10688
+ }).refine((v) => v.scope === "operator" || !!v.projectId, {
10689
+ message: "projectId is required for SDK and publishing keys",
9529
10690
  path: ["projectId"]
9530
10691
  });
9531
10692
  var boundedString = (max) => external_exports.string().max(max);
@@ -9550,6 +10711,15 @@ var snapshotInputSchema = external_exports.object({
9550
10711
  type: boundedString(40).optional(),
9551
10712
  required: external_exports.boolean().optional(),
9552
10713
  invalid: external_exports.boolean().optional(),
10714
+ /**
10715
+ * Whether the field holds anything, as its own fact rather than an
10716
+ * inference over `value`. The value is the user's and is the first
10717
+ * thing a privacy policy withholds; the existence of a value is the
10718
+ * agent's and is what stops it filling the same field twice.
10719
+ */
10720
+ filled: external_exports.boolean().optional(),
10721
+ /** This input has keyboard focus right now. */
10722
+ focused: external_exports.boolean().optional(),
9553
10723
  inModal: external_exports.boolean().optional()
9554
10724
  }).passthrough();
9555
10725
  var snapshotButtonSchema = external_exports.object({
@@ -9705,6 +10875,32 @@ var agentSnapshotSchema = external_exports.object({
9705
10875
  * it. Optional: older SDKs never clamp and never send it.
9706
10876
  */
9707
10877
  truncated: external_exports.boolean().optional(),
10878
+ /**
10879
+ * What moved since the previous observation, computed on the device
10880
+ * because that is the only side holding both snapshots.
10881
+ *
10882
+ * Shape only — counts, booleans, and ids the app declared itself — so
10883
+ * it survives an observation whose content was withheld. ABSENT means
10884
+ * there was no previous observation to compare against; `unchanged:
10885
+ * true` means we compared and nothing moved, which is the strongest
10886
+ * evidence available that an action did nothing. The two must not be
10887
+ * collapsed, for the same reason `truncated` exists.
10888
+ */
10889
+ delta: external_exports.object({
10890
+ routeChanged: external_exports.boolean().optional(),
10891
+ modalOpened: external_exports.boolean().optional(),
10892
+ modalClosed: external_exports.boolean().optional(),
10893
+ loadingStarted: external_exports.boolean().optional(),
10894
+ loadingFinished: external_exports.boolean().optional(),
10895
+ textsAdded: external_exports.number().int().nonnegative().optional(),
10896
+ textsRemoved: external_exports.number().int().nonnegative().optional(),
10897
+ visibleRowsDelta: external_exports.number().int().optional(),
10898
+ totalRowsDelta: external_exports.number().int().optional(),
10899
+ fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
10900
+ fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
10901
+ invalidAppeared: external_exports.boolean().optional(),
10902
+ unchanged: external_exports.boolean().optional()
10903
+ }).passthrough().optional(),
9708
10904
  /**
9709
10905
  * How many of this screen's controls the client could name, split by
9710
10906
  * `identityProvenance`. Diagnostic only — the relay never grounds an
@@ -9738,7 +10934,19 @@ var agentContextSchema = external_exports.object({
9738
10934
  rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
9739
10935
  currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
9740
10936
  routeNames: external_exports.array(boundedString(200)).max(500).optional(),
9741
- canGoBack: external_exports.boolean().optional()
10937
+ canGoBack: external_exports.boolean().optional(),
10938
+ /**
10939
+ * The stack a back press pops through, oldest first, ending on
10940
+ * the current screen. `canGoBack` says a back exists; this says
10941
+ * where it goes.
10942
+ */
10943
+ backStack: external_exports.array(boundedString(200)).max(50).optional(),
10944
+ /**
10945
+ * Screens the session has been on, oldest first. Names only —
10946
+ * params carry record ids and often personal data, and a route
10947
+ * name is structure.
10948
+ */
10949
+ visited: external_exports.array(boundedString(200)).max(8).optional()
9742
10950
  }).passthrough().optional(),
9743
10951
  screenMetadata: external_exports.object({
9744
10952
  name: boundedString(200).optional(),
@@ -9844,7 +11052,7 @@ var formFillPayloadSchema = external_exports.object({
9844
11052
  submitAfterFill: external_exports.boolean().optional()
9845
11053
  }).passthrough();
9846
11054
  var uiInteractionPayloadSchema = external_exports.object({
9847
- action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
11055
+ action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
9848
11056
  // `targetId` is the canonical server/LLM field. SDK runtimes still
9849
11057
  // accept `componentId` as a compatibility alias.
9850
11058
  targetId: external_exports.string().min(1),
@@ -10012,8 +11220,15 @@ var continueAgentSchema = external_exports.object({
10012
11220
  // issue #169 — the observation the server grounds targets against, now
10013
11221
  // validated + bounded at the border instead of z.record(z.unknown()).
10014
11222
  context: agentContextSchema.optional(),
10015
- /** Hop counter — server enforces a cap to prevent runaway loops */
10016
- hop: external_exports.number().int().min(1).max(10).optional()
11223
+ /**
11224
+ * Hop counter — server enforces a cap to prevent runaway loops.
11225
+ * The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
11226
+ * #503, apps/api/src/modules/agents/routes.ts): the first over-budget
11227
+ * hop has to get through validation so the route can answer it with
11228
+ * the friendly automation-limit message instead of a 422. The +3
11229
+ * headroom mirrors what 7/10 was before the recalibration.
11230
+ */
11231
+ hop: external_exports.number().int().min(1).max(13).optional()
10017
11232
  });
10018
11233
  var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
10019
11234
  var screenPermissionSchema = external_exports.object({
@@ -10119,15 +11334,18 @@ var themeTokensSchema = external_exports.object({
10119
11334
  */
10120
11335
  mode: external_exports.enum(["auto", "light", "dark"]).optional()
10121
11336
  }).strict();
11337
+ var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
11338
+ message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
11339
+ });
10122
11340
  var projectPersonalizationSchema = external_exports.object({
10123
11341
  /** Tone / persona instructions appended to the system prompt. */
10124
11342
  personaPrompt: external_exports.string().max(2048).nullable(),
10125
11343
  /** Display name in the chat header (e.g. "Aria"). */
10126
11344
  assistantName: external_exports.string().max(60).nullable(),
10127
11345
  /** URL or remote asset id for the avatar shown next to assistant turns. */
10128
- assistantAvatar: external_exports.string().max(500).nullable(),
11346
+ assistantAvatar: imageSourceField.nullable(),
10129
11347
  /** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
10130
- emptyStateIcon: external_exports.string().max(500).nullable(),
11348
+ emptyStateIcon: imageSourceField.nullable(),
10131
11349
  /** First-message text shown in the empty state. */
10132
11350
  welcomeMessage: external_exports.string().max(500).nullable(),
10133
11351
  /** Title rendered at the top of the chat. */
@@ -10141,20 +11359,20 @@ var projectPersonalizationSchema = external_exports.object({
10141
11359
  /** Chat-open FAB background color. Null falls back to theme.colors.primary. */
10142
11360
  triggerButtonColor: hexColorSchema.nullable(),
10143
11361
  /** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
10144
- triggerButtonImageUrl: external_exports.string().max(500).nullable()
11362
+ triggerButtonImageUrl: imageSourceField.nullable()
10145
11363
  });
10146
11364
  var updateProjectPersonalizationSchema = external_exports.object({
10147
11365
  personaPrompt: external_exports.string().max(2048).nullable().optional(),
10148
11366
  assistantName: external_exports.string().max(60).nullable().optional(),
10149
- assistantAvatar: external_exports.string().max(500).nullable().optional(),
10150
- emptyStateIcon: external_exports.string().max(500).nullable().optional(),
11367
+ assistantAvatar: imageSourceField.nullable().optional(),
11368
+ emptyStateIcon: imageSourceField.nullable().optional(),
10151
11369
  welcomeMessage: external_exports.string().max(500).nullable().optional(),
10152
11370
  chatTitle: external_exports.string().max(60).nullable().optional(),
10153
11371
  poweredByVisible: external_exports.boolean().optional(),
10154
11372
  theme: themeTokensSchema.nullable().optional(),
10155
11373
  defaultLocale: localeSchema.nullable().optional(),
10156
11374
  triggerButtonColor: hexColorSchema.nullable().optional(),
10157
- triggerButtonImageUrl: external_exports.string().max(500).nullable().optional()
11375
+ triggerButtonImageUrl: imageSourceField.nullable().optional()
10158
11376
  }).strict();
10159
11377
  var sandboxObservationSchema = external_exports.object({
10160
11378
  route: external_exports.string().max(200).optional(),
@@ -10174,7 +11392,19 @@ var sandboxObservationSchema = external_exports.object({
10174
11392
  type: external_exports.string().max(40).optional(),
10175
11393
  placeholder: external_exports.string().max(200).optional(),
10176
11394
  /** Inline validation state (SDK snapshot field). */
10177
- invalid: external_exports.boolean().optional()
11395
+ invalid: external_exports.boolean().optional(),
11396
+ /**
11397
+ * Whether the field holds anything, as its own fact rather than
11398
+ * an inference over `value`.
11399
+ *
11400
+ * Here because the eval posts through this schema: a fact the
11401
+ * SDK produces and this contract has no word for is a fact the
11402
+ * corpus can never grade the agent on. `invalid` and `required`
11403
+ * were already here; these two were the half that was missing.
11404
+ */
11405
+ filled: external_exports.boolean().optional(),
11406
+ /** This input has keyboard focus right now. */
11407
+ focused: external_exports.boolean().optional()
10178
11408
  })
10179
11409
  ).max(100).optional(),
10180
11410
  buttons: external_exports.array(
@@ -10632,7 +11862,7 @@ var manifestSchema = external_exports.object({
10632
11862
  navigation: navigationGraphSchema.partial().optional()
10633
11863
  }).passthrough();
10634
11864
  async function loadManifest(rootDir, manifestPath) {
10635
- const resolvedPath = import_node_path2.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
11865
+ const resolvedPath = import_node_path4.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
10636
11866
  let raw;
10637
11867
  try {
10638
11868
  raw = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
@@ -10781,7 +12011,7 @@ var MCPGenerator = class _MCPGenerator {
10781
12011
  const agentReadyScreens = mergedScreens.map(
10782
12012
  (screen) => enrichScreenForAgent({
10783
12013
  ...screen,
10784
- filePath: screen.filePath ? import_node_path3.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
12014
+ filePath: screen.filePath ? import_node_path5.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
10785
12015
  })
10786
12016
  );
10787
12017
  const projectInfo = await this.getProjectInfo();
@@ -10805,11 +12035,11 @@ var MCPGenerator = class _MCPGenerator {
10805
12035
  }
10806
12036
  };
10807
12037
  const serialized = JSON.stringify(document, null, 2);
10808
- const checksum = this.calculateChecksum(serialized);
10809
- const filePath = import_node_path3.default.resolve(outputDir, `mcp-document.${this.options.format}`);
12038
+ const checksum = this.calculateChecksum(serializeForChecksum(document));
12039
+ const filePath = import_node_path5.default.resolve(outputDir, `mcp-document.${this.options.format}`);
10810
12040
  await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
10811
12041
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10812
- const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
12042
+ const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
10813
12043
  await (0, import_promises4.writeFile)(checksumFilePath, checksum, "utf-8");
10814
12044
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
10815
12045
  console.log("[MCPGenerator] Generation complete!");
@@ -10817,7 +12047,8 @@ var MCPGenerator = class _MCPGenerator {
10817
12047
  document,
10818
12048
  filePath,
10819
12049
  format: this.options.format,
10820
- checksum
12050
+ checksum,
12051
+ ...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
10821
12052
  };
10822
12053
  }
10823
12054
  /**
@@ -10829,7 +12060,7 @@ var MCPGenerator = class _MCPGenerator {
10829
12060
  * after calling `generate()`.
10830
12061
  */
10831
12062
  static async readPreviousChecksum(outputDir) {
10832
- const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
12063
+ const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
10833
12064
  try {
10834
12065
  const content = await (0, import_promises4.readFile)(checksumFilePath, "utf-8");
10835
12066
  return content.trim() || null;
@@ -10848,7 +12079,7 @@ var MCPGenerator = class _MCPGenerator {
10848
12079
  */
10849
12080
  async getProjectInfo() {
10850
12081
  try {
10851
- const packageJsonPath = import_node_path3.default.resolve(this.analyzerConfig.rootDir, "package.json");
12082
+ const packageJsonPath = import_node_path5.default.resolve(this.analyzerConfig.rootDir, "package.json");
10852
12083
  const packageJsonContent = await (0, import_promises4.readFile)(packageJsonPath, "utf-8");
10853
12084
  const packageJson = JSON.parse(packageJsonContent);
10854
12085
  return {
@@ -10923,8 +12154,8 @@ function reportMetadataLint(document, strict) {
10923
12154
  }
10924
12155
 
10925
12156
  // src/cli/utils/remote-drift.ts
10926
- var import_node_fs = require("fs");
10927
- var import_node_path4 = require("path");
12157
+ var import_node_fs3 = require("fs");
12158
+ var import_node_path6 = require("path");
10928
12159
  var STUB_RATIO = 0.5;
10929
12160
  function compareWithRemote(local, remote) {
10930
12161
  if (remote.screensCount === local.screens) return null;
@@ -10937,7 +12168,7 @@ function compareWithRemote(local, remote) {
10937
12168
  }
10938
12169
  function readLocalDocumentSummary(outputDir) {
10939
12170
  try {
10940
- const raw = (0, import_node_fs.readFileSync)((0, import_node_path4.join)(outputDir, "mcp-document.json"), "utf-8");
12171
+ const raw = (0, import_node_fs3.readFileSync)((0, import_node_path6.join)(outputDir, "mcp-document.json"), "utf-8");
10941
12172
  const parsed = JSON.parse(raw);
10942
12173
  if (typeof parsed?.metadata?.totalScreens !== "number") return null;
10943
12174
  return { screens: parsed.metadata.totalScreens };
@@ -11022,14 +12253,18 @@ function assertDocumentHasScreens(document, options = {}) {
11022
12253
  }
11023
12254
  error("No screens found \u2014 this application map would give the agent nothing to navigate.");
11024
12255
  info("");
11025
- info("The analyzer looks for React Navigation: a <NavigationContainer> and");
11026
- info("<Stack.Screen> / <Tab.Screen> declarations. It found none.");
12256
+ info("The analyzer reads React Navigation \u2014 <X.Navigator> JSX and the static");
12257
+ info("`createXNavigator({ screens })` config \u2014 and Expo Router directory trees.");
12258
+ info("It found no screen in any of them.");
11027
12259
  info("");
11028
12260
  info("The two usual causes:");
11029
- info(" \u2022 The app uses Expo Router (or another file-based router). It is the");
11030
- info(" default for `npx create-expo-app` and the analyzer cannot read it.");
11031
- info(" \u2022 The screens live outside the analyzed paths \u2014 check `include`,");
11032
- info(" `exclude` and `screenPatterns` in your Appilots config.");
12261
+ info(" \u2022 The screens live outside the analyzed paths, or are named in a way");
12262
+ info(" the screen filter does not match \u2014 check `include`, `exclude` and");
12263
+ info(" `screenPatterns` in your Appilots config. The scan summary printed");
12264
+ info(" above says how many files were reached and how many were filtered.");
12265
+ info(" \u2022 The app uses a router the analyzer does not read yet, such as");
12266
+ info(" react-native-navigation (Wix). Declaring the screens works for any");
12267
+ info(" router \u2014 see the escape hatch below.");
11033
12268
  info("");
11034
12269
  info("The escape hatch, which works for any router: declare your screens in");
11035
12270
  info("an `appilots.manifest.json` at the project root. Its screens are merged");
@@ -11076,7 +12311,7 @@ function syncCommand() {
11076
12311
  const outputDir = config.outputDir || ".appilots";
11077
12312
  let previousChecksum = "";
11078
12313
  try {
11079
- const checksumPath = (0, import_node_path5.join)(outputDir, ".appilots-checksum");
12314
+ const checksumPath = (0, import_node_path7.join)(outputDir, ".appilots-checksum");
11080
12315
  previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
11081
12316
  } catch {
11082
12317
  }
@@ -11203,7 +12438,7 @@ function syncCommand() {
11203
12438
 
11204
12439
  // src/cli/commands/watch.ts
11205
12440
  var import_commander3 = require("commander");
11206
- var import_node_fs2 = require("fs");
12441
+ var import_node_fs4 = require("fs");
11207
12442
  function watchCommand() {
11208
12443
  return new import_commander3.Command("watch").description("Watch project for changes and auto-sync MCP documents").option("--verbose", "Enable verbose logging", false).action(async (options) => {
11209
12444
  try {
@@ -11250,7 +12485,7 @@ function watchCommand() {
11250
12485
  dim("Press Ctrl+C to stop\n");
11251
12486
  let debounceTimer = null;
11252
12487
  const DEBOUNCE_MS = 500;
11253
- const watcher = (0, import_node_fs2.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
12488
+ const watcher = (0, import_node_fs4.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
11254
12489
  if (!filename) return;
11255
12490
  if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
11256
12491
  return;
@@ -11302,6 +12537,95 @@ function watchCommand() {
11302
12537
 
11303
12538
  // src/cli/commands/generate.ts
11304
12539
  var import_commander4 = require("commander");
12540
+
12541
+ // src/cli/utils/reach-report.ts
12542
+ function actionableScreens(document) {
12543
+ return (document.screens ?? []).filter(
12544
+ (screen) => (screen.actions?.length ?? 0) > 0 || (screen.forms?.length ?? 0) > 0 || (screen.targets?.length ?? 0) > 0
12545
+ ).length;
12546
+ }
12547
+ function pct(part, whole) {
12548
+ if (whole <= 0) return "\u2014";
12549
+ return `${Math.round(100 * part / whole)}%`;
12550
+ }
12551
+ function reportReach(input) {
12552
+ const { document, navigation } = input;
12553
+ const screens = document.screens?.length ?? 0;
12554
+ const useful = actionableScreens(document);
12555
+ const filteredOut = document.metadata?.screensFilteredOut ?? 0;
12556
+ info("");
12557
+ info("Alcance da varredura");
12558
+ info(
12559
+ ` telas ${screens} de ${document.metadata?.analyzedFiles ?? 0} arquivos analisados` + (filteredOut > 0 ? `, ${filteredOut} fora pelo filtro de tela` : "")
12560
+ );
12561
+ info(` com aford\xE2ncia ${useful} (${pct(useful, screens)}) \u2014 a\xE7\xE3o, formul\xE1rio ou target`);
12562
+ if (navigation) {
12563
+ info(
12564
+ ` navega\xE7\xE3o ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos com evid\xEAncia, ${navigation.navigatorsWithJsx} navegador(es) no documento`
12565
+ );
12566
+ info(
12567
+ ` rotas ${navigation.routes}, ${navigation.routesLinkedToFile} (${pct(navigation.routesLinkedToFile, navigation.routes)}) ligadas a um arquivo de tela`
12568
+ );
12569
+ if (navigation.fileBasedRouter) {
12570
+ info(
12571
+ ` roteador ${navigation.fileBasedRouter.kind} em "${navigation.fileBasedRouter.routeDir}"`
12572
+ );
12573
+ }
12574
+ }
12575
+ const notes = [];
12576
+ if (navigation && navigation.routes > 0 && useful > navigation.routes * 5) {
12577
+ notes.push([
12578
+ `${useful} tela(s) com aford\xE2ncia para apenas ${navigation.routes} rota(s).`,
12579
+ "A varredura achou o que tocar em muito mais lugares do que sabe alcan\xE7ar.",
12580
+ `S\xF3 ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos varridos mencionam navega\xE7\xE3o \u2014`,
12581
+ "se o roteador deste app mora fora deles, `navigationInclude` no `.appilotsrc`",
12582
+ "estende a varredura sem mexer no resto."
12583
+ ]);
12584
+ }
12585
+ if (navigation && navigation.routes === 0 && useful > 0) {
12586
+ notes.push([
12587
+ `${useful} tela(s) com aford\xE2ncia e nenhuma rota: o agente v\xEA o que tocar e n\xE3o sabe chegar l\xE1.`,
12588
+ "O extractor l\xEA React Navigation (JSX e configura\xE7\xE3o est\xE1tica) e expo-router.",
12589
+ "Se o app usa outro roteador, declare as telas em `appilots.manifest.json` \u2014",
12590
+ "elas s\xE3o mescladas por cima do que a an\xE1lise achou."
12591
+ ]);
12592
+ } else if (navigation && navigation.routes > 0 && navigation.routesLinkedToFile < navigation.routes / 2) {
12593
+ notes.push([
12594
+ `${navigation.routes - navigation.routesLinkedToFile} de ${navigation.routes} rotas n\xE3o dizem qual arquivo montam.`,
12595
+ "A liga\xE7\xE3o vem de `component={X}`. Formas indiretas \u2014 `getComponent={() => X}`,",
12596
+ "componente constru\xEDdo em runtime \u2014 n\xE3o resolvem, e o agente fica com destinos vazios."
12597
+ ]);
12598
+ }
12599
+ if (screens > 0 && useful === 0) {
12600
+ notes.push([
12601
+ `${screens} tela(s) encontradas e nenhuma com aford\xE2ncia.`,
12602
+ "O extractor mira `onPress`/`onChangeText` com um locator endere\xE7\xE1vel:",
12603
+ "`testID`, `appilotsId`, `accessibilityLabel` ou r\xF3tulo literal.",
12604
+ "Sem nenhum deles n\xE3o h\xE1 como o agente apontar para o elemento."
12605
+ ]);
12606
+ }
12607
+ if (filteredOut > 0 && screens > 0 && filteredOut > screens * 9) {
12608
+ notes.push([
12609
+ `${filteredOut} arquivos ficaram fora do filtro de tela para ${screens} dentro.`,
12610
+ "Se as telas deste app moram noutro lugar, uma linha de `screenPatterns` no",
12611
+ "`.appilotsrc` aponta onde \u2014 \xE9 o degrau mais barato da escada."
12612
+ ]);
12613
+ }
12614
+ if (navigation && navigation.filesScanned > 0 && navigation.filesWithEvidence === 0) {
12615
+ notes.push([
12616
+ `Nenhum dos ${navigation.filesScanned} arquivos varridos menciona navega\xE7\xE3o.`,
12617
+ "Confira `include`/`exclude` no `.appilotsrc`: o extractor pode n\xE3o estar",
12618
+ "alcan\xE7ando o diret\xF3rio onde o roteador \xE9 declarado."
12619
+ ]);
12620
+ }
12621
+ for (const note of notes) {
12622
+ info("");
12623
+ warn(note[0]);
12624
+ for (const line of note.slice(1)) info(` ${line}`);
12625
+ }
12626
+ }
12627
+
12628
+ // src/cli/commands/generate.ts
11305
12629
  function generateCommand() {
11306
12630
  return new import_commander4.Command("generate").description("Generate MCP documents from project source (local only)").option("-o, --output <path>", "Output directory for MCP documents", ".appilots").option("-f, --format <format>", "Output format (only json supported)", "json").option(
11307
12631
  "--strict-metadata",
@@ -11318,7 +12642,7 @@ function generateCommand() {
11318
12642
  spinner.start();
11319
12643
  let loaded = null;
11320
12644
  try {
11321
- loaded = loadConfig(warn);
12645
+ loaded = loadConfig(warn, { requireApiKey: false });
11322
12646
  } catch {
11323
12647
  loaded = null;
11324
12648
  }
@@ -11326,11 +12650,21 @@ function generateCommand() {
11326
12650
  rootDir: process.cwd(),
11327
12651
  outputDir: options.output || ".appilots",
11328
12652
  format: "json",
12653
+ strictScreens: loaded?.strictScreens,
12654
+ screenPatterns: loaded?.screenPatterns,
12655
+ include: loaded?.include,
12656
+ exclude: loaded?.exclude,
12657
+ navigationInclude: loaded?.navigationInclude,
12658
+ navigationExclude: loaded?.navigationExclude,
11329
12659
  platform: loaded?.platform,
11330
12660
  manifestPath: loaded?.manifestPath
11331
12661
  });
11332
12662
  const output = await generator.generate();
11333
12663
  spinner.stop();
12664
+ reportReach({
12665
+ document: output.document,
12666
+ ...output.diagnostics?.navigation ? { navigation: output.diagnostics.navigation } : {}
12667
+ });
11334
12668
  assertDocumentHasScreens(output.document, {
11335
12669
  rootDir: process.cwd(),
11336
12670
  allowEmpty: Boolean(options.allowEmpty)
@@ -11350,19 +12684,18 @@ function generateCommand() {
11350
12684
 
11351
12685
  // src/cli/commands/annotate.ts
11352
12686
  var import_promises6 = require("fs/promises");
11353
- var import_node_path7 = __toESM(require("path"));
12687
+ var import_node_path9 = __toESM(require("path"));
11354
12688
  var import_commander5 = require("commander");
11355
- var import_fast_glob6 = __toESM(require("fast-glob"));
11356
12689
 
11357
12690
  // src/annotate/sites.ts
11358
12691
  var import_traverse12 = __toESM(require("@babel/traverse"));
11359
- var t13 = __toESM(require("@babel/types"));
12692
+ var t14 = __toESM(require("@babel/types"));
11360
12693
 
11361
12694
  // src/annotate/forwarding.ts
11362
- var import_node_path6 = __toESM(require("path"));
11363
- var import_node_fs3 = require("fs");
12695
+ var import_node_path8 = __toESM(require("path"));
12696
+ var import_node_fs5 = require("fs");
11364
12697
  var import_traverse11 = __toESM(require("@babel/traverse"));
11365
- var t12 = __toESM(require("@babel/types"));
12698
+ var t13 = __toESM(require("@babel/types"));
11366
12699
  var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
11367
12700
  var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
11368
12701
  "View",
@@ -11394,10 +12727,10 @@ function collectImports(ast) {
11394
12727
  ImportDeclaration(nodePath) {
11395
12728
  const source = nodePath.node.source.value;
11396
12729
  for (const spec of nodePath.node.specifiers) {
11397
- if (t12.isImportSpecifier(spec)) {
11398
- const imported = t12.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
12730
+ if (t13.isImportSpecifier(spec)) {
12731
+ const imported = t13.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
11399
12732
  out.set(spec.local.name, { source, local: spec.local.name, imported });
11400
- } else if (t12.isImportDefaultSpecifier(spec)) {
12733
+ } else if (t13.isImportDefaultSpecifier(spec)) {
11401
12734
  out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
11402
12735
  }
11403
12736
  }
@@ -11405,17 +12738,17 @@ function collectImports(ast) {
11405
12738
  });
11406
12739
  return out;
11407
12740
  }
11408
- var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
12741
+ var EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
11409
12742
  function resolveRelativeImport(fromFile, source) {
11410
12743
  if (!source.startsWith(".")) return void 0;
11411
- const base = import_node_path6.default.resolve(import_node_path6.default.dirname(fromFile), source);
11412
- for (const ext of EXTENSIONS) {
12744
+ const base = import_node_path8.default.resolve(import_node_path8.default.dirname(fromFile), source);
12745
+ for (const ext of EXTENSIONS2) {
11413
12746
  const candidate = `${base}${ext}`;
11414
- if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
12747
+ if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
11415
12748
  }
11416
- for (const ext of EXTENSIONS) {
11417
- const candidate = import_node_path6.default.join(base, `index${ext}`);
11418
- if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
12749
+ for (const ext of EXTENSIONS2) {
12750
+ const candidate = import_node_path8.default.join(base, `index${ext}`);
12751
+ if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
11419
12752
  }
11420
12753
  return void 0;
11421
12754
  }
@@ -11430,7 +12763,7 @@ function inspectModule(file2, exportName, depth = 0) {
11430
12763
  if (depth > 2) return none;
11431
12764
  let source;
11432
12765
  try {
11433
- source = (0, import_node_fs3.readFileSync)(file2, "utf-8");
12766
+ source = (0, import_node_fs5.readFileSync)(file2, "utf-8");
11434
12767
  } catch {
11435
12768
  return none;
11436
12769
  }
@@ -11451,7 +12784,7 @@ function inspectModule(file2, exportName, depth = 0) {
11451
12784
  const from = nodePath.node.source?.value;
11452
12785
  if (!from) return;
11453
12786
  const named = nodePath.node.specifiers.some(
11454
- (spec) => t12.isExportSpecifier(spec) && (t12.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
12787
+ (spec) => t13.isExportSpecifier(spec) && (t13.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
11455
12788
  );
11456
12789
  if (named) barrelTarget = from;
11457
12790
  },
@@ -11460,11 +12793,11 @@ function inspectModule(file2, exportName, depth = 0) {
11460
12793
  },
11461
12794
  // `interface CardProps { testID?: string }` / `type Props = { testID… }`
11462
12795
  TSPropertySignature(nodePath) {
11463
- if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
12796
+ if (t13.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11464
12797
  },
11465
12798
  // `function Card({ testID }) {}` — the destructured parameter.
11466
12799
  ObjectProperty(nodePath) {
11467
- if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
12800
+ if (t13.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11468
12801
  },
11469
12802
  // `<View {...props} />` — anything spread onto JSX carries it along.
11470
12803
  JSXSpreadAttribute() {
@@ -11475,17 +12808,17 @@ function inspectModule(file2, exportName, depth = 0) {
11475
12808
  // that is one hardcoded name shared by every instance, which is the
11476
12809
  // duplicate-id problem rather than a solution to it.
11477
12810
  JSXAttribute(nodePath) {
11478
- const name = t12.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
12811
+ const name = t13.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
11479
12812
  if (name !== "testID" && name !== "accessibilityLabel") return;
11480
12813
  const value = nodePath.node.value;
11481
- if (!t12.isJSXExpressionContainer(value)) return;
11482
- if (t12.isStringLiteral(value.expression)) return;
12814
+ if (!t13.isJSXExpressionContainer(value)) return;
12815
+ if (t13.isStringLiteral(value.expression)) return;
11483
12816
  derives = true;
11484
12817
  },
11485
12818
  // `useAppilotsTarget(`${groupId}-${option.value}`, …)`
11486
12819
  CallExpression(nodePath) {
11487
12820
  const callee = nodePath.node.callee;
11488
- if (t12.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
12821
+ if (t13.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
11489
12822
  }
11490
12823
  });
11491
12824
  if (forwards || derives) return { forwards, derives };
@@ -11570,18 +12903,18 @@ function hasLabelText(element) {
11570
12903
  function namedHandler(element, prop) {
11571
12904
  if (!prop) return void 0;
11572
12905
  const attr = findJsxAttribute(element, prop);
11573
- if (!attr || !t13.isJSXExpressionContainer(attr.value)) return void 0;
11574
- return t13.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
12906
+ if (!attr || !t14.isJSXExpressionContainer(attr.value)) return void 0;
12907
+ return t14.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
11575
12908
  }
11576
12909
  var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
11577
12910
  function isPerItemRender(nodePath) {
11578
12911
  let current = nodePath.parentPath;
11579
12912
  while (current) {
11580
12913
  const node = current.node;
11581
- if (t13.isCallExpression(node) && t13.isMemberExpression(node.callee) && t13.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
12914
+ if (t14.isCallExpression(node) && t14.isMemberExpression(node.callee) && t14.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
11582
12915
  return true;
11583
12916
  }
11584
- if (t13.isJSXAttribute(node) && t13.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
12917
+ if (t14.isJSXAttribute(node) && t14.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
11585
12918
  return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
11586
12919
  }
11587
12920
  current = current.parentPath;
@@ -11611,7 +12944,7 @@ function findAnnotationSites(source, file2) {
11611
12944
  traverse12(ast, {
11612
12945
  JSXOpeningElement(nodePath) {
11613
12946
  const element = nodePath.node;
11614
- if (!t13.isJSXIdentifier(element.name)) return;
12947
+ if (!t14.isJSXIdentifier(element.name)) return;
11615
12948
  const component = element.name.name;
11616
12949
  const role = classifyJsxComponent(component, element);
11617
12950
  if (!ANNOTATABLE_ROLES.has(role)) return;
@@ -11693,10 +13026,10 @@ function applyToFile(source, sites) {
11693
13026
  }
11694
13027
 
11695
13028
  // src/annotate/forward.ts
11696
- var import_node_fs4 = require("fs");
13029
+ var import_node_fs6 = require("fs");
11697
13030
  var import_magic_string2 = __toESM(require("magic-string"));
11698
13031
  var import_traverse13 = __toESM(require("@babel/traverse"));
11699
- var t14 = __toESM(require("@babel/types"));
13032
+ var t15 = __toESM(require("@babel/types"));
11700
13033
  var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
11701
13034
  function findComponent(ast, name) {
11702
13035
  let found;
@@ -11707,9 +13040,9 @@ function findComponent(ast, name) {
11707
13040
  }
11708
13041
  },
11709
13042
  VariableDeclarator(nodePath) {
11710
- if (!t14.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
13043
+ if (!t15.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
11711
13044
  const init = nodePath.node.init;
11712
- if (t14.isArrowFunctionExpression(init) || t14.isFunctionExpression(init)) {
13045
+ if (t15.isArrowFunctionExpression(init) || t15.isFunctionExpression(init)) {
11713
13046
  found ??= { params: init.params, body: init.body };
11714
13047
  }
11715
13048
  }
@@ -11721,26 +13054,26 @@ function findReturnedRoots(body) {
11721
13054
  let unsupported = false;
11722
13055
  const record = (node) => {
11723
13056
  if (!node) return;
11724
- if (t14.isParenthesizedExpression(node)) return record(node.expression);
11725
- if (t14.isJSXElement(node)) {
13057
+ if (t15.isParenthesizedExpression(node)) return record(node.expression);
13058
+ if (t15.isJSXElement(node)) {
11726
13059
  roots.push(node.openingElement);
11727
13060
  return;
11728
13061
  }
11729
- if (t14.isConditionalExpression(node)) {
13062
+ if (t15.isConditionalExpression(node)) {
11730
13063
  record(node.consequent);
11731
13064
  record(node.alternate);
11732
13065
  return;
11733
13066
  }
11734
- if (t14.isJSXFragment(node)) {
13067
+ if (t15.isJSXFragment(node)) {
11735
13068
  unsupported = true;
11736
13069
  return;
11737
13070
  }
11738
- if (t14.isNullLiteral(node)) return;
13071
+ if (t15.isNullLiteral(node)) return;
11739
13072
  unsupported = true;
11740
13073
  };
11741
- if (t14.isBlockStatement(body)) {
13074
+ if (t15.isBlockStatement(body)) {
11742
13075
  traverse13(
11743
- t14.file(t14.program([t14.expressionStatement(t14.functionExpression(null, [], body))])),
13076
+ t15.file(t15.program([t15.expressionStatement(t15.functionExpression(null, [], body))])),
11744
13077
  {
11745
13078
  ReturnStatement(nodePath) {
11746
13079
  record(nodePath.node.argument);
@@ -11754,7 +13087,7 @@ function findReturnedRoots(body) {
11754
13087
  return roots;
11755
13088
  }
11756
13089
  function isCarrierRoot(element, imports) {
11757
- if (!t14.isJSXIdentifier(element.name)) return false;
13090
+ if (!t15.isJSXIdentifier(element.name)) return false;
11758
13091
  const name = element.name.name;
11759
13092
  const origin = imports.get(name);
11760
13093
  if (origin?.source !== "react-native") return false;
@@ -11762,7 +13095,7 @@ function isCarrierRoot(element, imports) {
11762
13095
  }
11763
13096
  function hasProp(element, name) {
11764
13097
  return element.attributes.some(
11765
- (attr) => t14.isJSXAttribute(attr) && t14.isJSXIdentifier(attr.name) && attr.name.name === name
13098
+ (attr) => t15.isJSXAttribute(attr) && t15.isJSXIdentifier(attr.name) && attr.name.name === name
11766
13099
  );
11767
13100
  }
11768
13101
  function indentOf(source, offset) {
@@ -11793,7 +13126,7 @@ function followBarrel(file2, ast, component) {
11793
13126
  const from = nodePath.node.source?.value;
11794
13127
  if (!from) return;
11795
13128
  const named = nodePath.node.specifiers.some(
11796
- (spec) => t14.isExportSpecifier(spec) && (t14.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
13129
+ (spec) => t15.isExportSpecifier(spec) && (t15.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
11797
13130
  );
11798
13131
  if (named) target ??= from;
11799
13132
  }
@@ -11804,7 +13137,7 @@ function planForward(file2, component, depth = 0) {
11804
13137
  const base = { file: file2, component, edits: [], roots: 0 };
11805
13138
  let source;
11806
13139
  try {
11807
- source = (0, import_node_fs4.readFileSync)(file2, "utf-8");
13140
+ source = (0, import_node_fs6.readFileSync)(file2, "utf-8");
11808
13141
  } catch {
11809
13142
  return { ...base, refusal: `cannot read ${file2}` };
11810
13143
  }
@@ -11821,7 +13154,7 @@ function planForward(file2, component, depth = 0) {
11821
13154
  return { ...base, refusal: `no function named ${component} in this file` };
11822
13155
  }
11823
13156
  const param = found.params[0];
11824
- if (!t14.isObjectPattern(param)) {
13157
+ if (!t15.isObjectPattern(param)) {
11825
13158
  return {
11826
13159
  ...base,
11827
13160
  refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
@@ -11837,7 +13170,7 @@ function planForward(file2, component, depth = 0) {
11837
13170
  const imports = collectImports(ast);
11838
13171
  const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
11839
13172
  if (nonCarrier) {
11840
- const name = t14.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
13173
+ const name = t15.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
11841
13174
  return {
11842
13175
  ...base,
11843
13176
  refusal: `${component} renders <${name}> at its root, which is not a React Native element known to accept testID. Forwarding into it would move the problem down a level, not fix it`
@@ -11845,21 +13178,21 @@ function planForward(file2, component, depth = 0) {
11845
13178
  }
11846
13179
  const edits = [];
11847
13180
  const alreadyDestructured = param.properties.some(
11848
- (prop) => t14.isObjectProperty(prop) && t14.isIdentifier(prop.key) && prop.key.name === "testID"
13181
+ (prop) => t15.isObjectProperty(prop) && t15.isIdentifier(prop.key) && prop.key.name === "testID"
11849
13182
  );
11850
13183
  if (!alreadyDestructured) {
11851
13184
  const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
11852
13185
  edits.push({ ...insert, what: "parameter" });
11853
13186
  }
11854
13187
  const annotation = param.typeAnnotation;
11855
- if (t14.isTSTypeAnnotation(annotation)) {
13188
+ if (t15.isTSTypeAnnotation(annotation)) {
11856
13189
  const declared = annotation.typeAnnotation;
11857
13190
  let body;
11858
13191
  let closing = 0;
11859
- if (t14.isTSTypeLiteral(declared)) {
13192
+ if (t15.isTSTypeLiteral(declared)) {
11860
13193
  body = declared.members;
11861
13194
  closing = declared.end ?? 0;
11862
- } else if (t14.isTSTypeReference(declared) && t14.isIdentifier(declared.typeName)) {
13195
+ } else if (t15.isTSTypeReference(declared) && t15.isIdentifier(declared.typeName)) {
11863
13196
  const typeName = declared.typeName.name;
11864
13197
  traverse13(ast, {
11865
13198
  TSInterfaceDeclaration(nodePath) {
@@ -11868,7 +13201,7 @@ function planForward(file2, component, depth = 0) {
11868
13201
  closing = nodePath.node.body.end ?? 0;
11869
13202
  },
11870
13203
  TSTypeAliasDeclaration(nodePath) {
11871
- if (nodePath.node.id.name !== typeName || !t14.isTSTypeLiteral(nodePath.node.typeAnnotation))
13204
+ if (nodePath.node.id.name !== typeName || !t15.isTSTypeLiteral(nodePath.node.typeAnnotation))
11872
13205
  return;
11873
13206
  body = nodePath.node.typeAnnotation.members;
11874
13207
  closing = nodePath.node.typeAnnotation.end ?? 0;
@@ -11883,7 +13216,7 @@ function planForward(file2, component, depth = 0) {
11883
13216
  }
11884
13217
  if (body) {
11885
13218
  const declaresTestId = body.some(
11886
- (member) => t14.isTSPropertySignature(member) && t14.isIdentifier(member.key) && member.key.name === "testID"
13219
+ (member) => t15.isTSPropertySignature(member) && t15.isIdentifier(member.key) && member.key.name === "testID"
11887
13220
  );
11888
13221
  if (!declaresTestId) {
11889
13222
  const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
@@ -11940,7 +13273,7 @@ function annotateCommand() {
11940
13273
  const spinner = createSpinner("Scanning for unaddressable controls...");
11941
13274
  spinner.start();
11942
13275
  resetForwardingCache();
11943
- const files = await (0, import_fast_glob6.default)(include, { cwd: rootDir, ignore: exclude, absolute: true });
13276
+ const files = await globSorted(include, { cwd: rootDir, ignore: exclude, absolute: true });
11944
13277
  const scan = async () => {
11945
13278
  const out = [];
11946
13279
  for (const file2 of files) {
@@ -11989,9 +13322,9 @@ function annotateCommand() {
11989
13322
  written: options.write === true,
11990
13323
  annotated: written.length,
11991
13324
  blocked: blocked.length,
11992
- files: touched.map((edit) => import_node_path7.default.relative(rootDir, edit.file)),
13325
+ files: touched.map((edit) => import_node_path9.default.relative(rootDir, edit.file)),
11993
13326
  sites: [...written, ...blocked].map((site) => ({
11994
- file: import_node_path7.default.relative(rootDir, site.file),
13327
+ file: import_node_path9.default.relative(rootDir, site.file),
11995
13328
  line: site.line,
11996
13329
  component: site.component,
11997
13330
  role: site.role,
@@ -12034,9 +13367,12 @@ async function planForwards(edits, rootDir) {
12034
13367
  void rootDir;
12035
13368
  return plans;
12036
13369
  }
13370
+ function escapeRegExp(value) {
13371
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13372
+ }
12037
13373
  function findImportSource(source, name) {
12038
13374
  const pattern = new RegExp(
12039
- String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
13375
+ String.raw`import\s*\{[^}]*\b${escapeRegExp(name)}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
12040
13376
  );
12041
13377
  return pattern.exec(source)?.[1];
12042
13378
  }
@@ -12045,7 +13381,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12045
13381
  if (written.length > 0) {
12046
13382
  heading(didWrite ? "Added" : "Would add");
12047
13383
  for (const edit of touched) {
12048
- info(import_node_path7.default.relative(rootDir, edit.file));
13384
+ info(import_node_path9.default.relative(rootDir, edit.file));
12049
13385
  for (const site of edit.written) {
12050
13386
  dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
12051
13387
  }
@@ -12057,7 +13393,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12057
13393
  if (teachable.size > 0) {
12058
13394
  heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
12059
13395
  for (const plan of teachable.values()) {
12060
- info(`<${plan.component}> ${import_node_path7.default.relative(rootDir, plan.file)}`);
13396
+ info(`<${plan.component}> ${import_node_path9.default.relative(rootDir, plan.file)}`);
12061
13397
  for (const edit of plan.edits) {
12062
13398
  dim(` ${edit.what}: ${edit.text.trim()}`);
12063
13399
  }
@@ -12365,10 +13701,10 @@ function matchActions(expected, actual, reply = "") {
12365
13701
  var import_fs9 = require("fs");
12366
13702
  var import_path8 = require("path");
12367
13703
  var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
12368
- function loadBaseline(path11) {
12369
- if (!(0, import_fs9.existsSync)(path11)) return { ...EMPTY_BASELINE, perScenario: {} };
13704
+ function loadBaseline(path13) {
13705
+ if (!(0, import_fs9.existsSync)(path13)) return { ...EMPTY_BASELINE, perScenario: {} };
12370
13706
  try {
12371
- const raw = JSON.parse((0, import_fs9.readFileSync)(path11, "utf-8"));
13707
+ const raw = JSON.parse((0, import_fs9.readFileSync)(path13, "utf-8"));
12372
13708
  return {
12373
13709
  generatedAt: raw.generatedAt ?? null,
12374
13710
  gitSha: raw.gitSha ?? null,
@@ -12378,9 +13714,9 @@ function loadBaseline(path11) {
12378
13714
  return { ...EMPTY_BASELINE, perScenario: {} };
12379
13715
  }
12380
13716
  }
12381
- function saveBaseline(path11, baseline) {
12382
- (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path11), { recursive: true });
12383
- (0, import_fs9.writeFileSync)(path11, `${JSON.stringify(baseline, null, 2)}
13717
+ function saveBaseline(path13, baseline) {
13718
+ (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path13), { recursive: true });
13719
+ (0, import_fs9.writeFileSync)(path13, `${JSON.stringify(baseline, null, 2)}
12384
13720
  `, "utf-8");
12385
13721
  }
12386
13722
  function buildBaseline(outcomes, meta) {
@@ -12408,11 +13744,11 @@ function diffAgainstBaseline(outcomes, baseline, maxTokenRegression) {
12408
13744
  continue;
12409
13745
  }
12410
13746
  if (outcome.status === "passed" && prior.status === "passed" && prior.totalTokens > 0 && outcome.totalTokens > prior.totalTokens * (1 + maxTokenRegression)) {
12411
- const pct = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
13747
+ const pct2 = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
12412
13748
  regressions.push({
12413
13749
  name: outcome.name,
12414
13750
  kind: "token-regression",
12415
- detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
13751
+ detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct2}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
12416
13752
  });
12417
13753
  }
12418
13754
  }