@appilots/cli 0.10.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.10.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 = {};
@@ -127,6 +127,18 @@ function loadConfig(onWarn) {
127
127
  } else if (!env.apiKey) {
128
128
  return null;
129
129
  }
130
+ const legacyWarnings = [];
131
+ if (fileConfig.serverUrl === void 0) {
132
+ for (const [alias, canonical] of Object.entries(KEY_ALIASES)) {
133
+ if (canonical !== "serverUrl" || fileConfig[alias] === void 0) continue;
134
+ fileConfig.serverUrl = fileConfig[alias];
135
+ delete fileConfig[alias];
136
+ legacyWarnings.push(
137
+ `legacy key "${alias}" \u2014 using it as "serverUrl"; rename it in .appilotsrc`
138
+ );
139
+ break;
140
+ }
141
+ }
130
142
  const merged = {
131
143
  serverUrl: DEFAULT_SERVER_URL,
132
144
  outputDir: ".appilots",
@@ -138,6 +150,24 @@ function loadConfig(onWarn) {
138
150
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
139
151
  };
140
152
  const validation = validateConfig(merged);
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
+ }
158
+ if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
159
+ for (const key of Object.keys(fileConfig)) {
160
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
161
+ if (suggestConfigKey(key) !== "serverUrl") continue;
162
+ validation.warnings = validation.warnings.filter(
163
+ (warning) => !warning.startsWith(`unknown key "${key}"`)
164
+ );
165
+ validation.errors.push(
166
+ `unknown key "${key}" looks like "serverUrl" \u2014 not falling back to ${DEFAULT_SERVER_URL}; rename the key to "serverUrl"`
167
+ );
168
+ validation.valid = false;
169
+ }
170
+ }
141
171
  if (onWarn) {
142
172
  const source = configPath ?? "environment variables";
143
173
  for (const warning of validation.warnings) {
@@ -737,12 +767,18 @@ function initCommand() {
737
767
  // src/cli/commands/sync.ts
738
768
  var import_commander2 = require("commander");
739
769
  var import_promises5 = require("fs/promises");
740
- var import_node_path5 = require("path");
770
+ var import_node_path7 = require("path");
741
771
 
742
772
  // src/generators/MCPGenerator.ts
743
773
  var import_promises4 = require("fs/promises");
744
774
  var import_node_crypto = require("crypto");
745
- 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
+ }
746
782
 
747
783
  // src/pipeline/enrichment.ts
748
784
  function enrichScreenForAgent(screen) {
@@ -820,7 +856,10 @@ function mergeTargets(targets) {
820
856
  for (const target of targets) {
821
857
  if (!target.id) continue;
822
858
  const existing = byId.get(target.id);
823
- 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
+ );
824
863
  }
825
864
  return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
826
865
  }
@@ -865,7 +904,10 @@ function synthesizeFlows(screen, targets) {
865
904
  intent: "destructive_action",
866
905
  steps: [
867
906
  { type: "press", target: action.id, label: action.label },
868
- { 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
+ },
869
911
  { type: "wait", description: describeWait(action) }
870
912
  ],
871
913
  waitPolicy: waitPolicyForAction(action),
@@ -880,10 +922,19 @@ function synthesizeFlows(screen, targets) {
880
922
  title: `Act on an item in ${collection.id}`,
881
923
  intent: "list_action",
882
924
  steps: [
883
- { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
884
- { 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
+ }
885
934
  ],
886
- waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
935
+ waitPolicy: {
936
+ expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
937
+ }
887
938
  });
888
939
  }
889
940
  }
@@ -891,25 +942,32 @@ function synthesizeFlows(screen, targets) {
891
942
  }
892
943
  function waitPolicyForAction(action) {
893
944
  const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
894
- 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);
895
949
  return {
896
950
  ...expectedOutcome ? { expectedOutcome } : {},
897
951
  ...signals && signals.length > 0 ? { signals } : {},
898
- ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
952
+ ...maxMs !== void 0 ? { maxMs } : {}
899
953
  };
900
954
  }
901
955
  function describeWait(action) {
902
956
  if (action.successSignal?.description) return action.successSignal.description;
903
- 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";
904
959
  if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
905
- if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
960
+ if (action.appilotsInferred?.expectedOutcome)
961
+ return `Wait for ${action.appilotsInferred.expectedOutcome}`;
906
962
  return "Wait for the UI to settle";
907
963
  }
908
964
  function synthesizeAgentHints(screen, targets, flows) {
909
965
  const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
910
966
  const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
911
967
  const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
912
- 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
+ );
913
971
  const hints = {
914
972
  primaryGoal: synthesizePrimaryGoal(screen),
915
973
  commonTasks,
@@ -936,7 +994,9 @@ function synthesizePrimaryGoal(screen) {
936
994
  }
937
995
  const fieldCount = uniqueFields.size;
938
996
  if (fieldCount > 0) {
939
- 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;
940
1000
  const fieldLabel = fieldCount === 1 ? "field" : "fields";
941
1001
  goals.push(
942
1002
  requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
@@ -950,7 +1010,9 @@ function synthesizePrimaryGoal(screen) {
950
1010
  if (submitCount > 0) {
951
1011
  goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
952
1012
  }
953
- const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
1013
+ const asyncCount = screen.actions.filter(
1014
+ (action) => action.appilotsInferred?.isAsyncTrigger
1015
+ ).length;
954
1016
  if (asyncCount > 0) {
955
1017
  goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
956
1018
  }
@@ -960,15 +1022,23 @@ function synthesizePrimaryGoal(screen) {
960
1022
  }
961
1023
 
962
1024
  // src/analyzers/ReactNativePlatformAnalyzer.ts
963
- var import_node_path = __toESM(require("path"));
964
- 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
+ }
965
1036
 
966
1037
  // src/analyzers/ScreenAnalyzer.ts
967
1038
  var import_promises = __toESM(require("fs/promises"));
968
1039
  var import_path3 = __toESM(require("path"));
969
1040
  var import_traverse4 = __toESM(require("@babel/traverse"));
970
1041
  var BabelTypes = __toESM(require("@babel/types"));
971
- var import_fast_glob = __toESM(require("fast-glob"));
972
1042
 
973
1043
  // src/ast/parse.ts
974
1044
  var import_parser = require("@babel/parser");
@@ -1371,14 +1441,25 @@ var ScreenAnalyzer = class {
1371
1441
  strictScreens;
1372
1442
  /** Glob patterns that identify screen files in strict mode */
1373
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;
1374
1454
  /** §D: Count of screens filtered out in strict mode (available after analyze()) */
1375
1455
  screensFilteredOut = 0;
1376
1456
  constructor(config, options) {
1377
1457
  this.config = config;
1458
+ this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
1378
1459
  this.strictScreens = options?.strictScreens ?? false;
1379
1460
  this.screenPatterns = options?.screenPatterns ?? [
1380
- "**/*Screen.{ts,tsx}",
1381
- "**/screens/**/*.{ts,tsx}"
1461
+ "**/*Screen.{ts,tsx,js,jsx}",
1462
+ "**/screens/**/*.{ts,tsx,js,jsx}"
1382
1463
  ];
1383
1464
  }
1384
1465
  /** Analyze all screens in the project */
@@ -1390,7 +1471,7 @@ var ScreenAnalyzer = class {
1390
1471
  console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
1391
1472
  }
1392
1473
  }
1393
- const files = await (0, import_fast_glob.default)(include, {
1474
+ const files = await globSorted(include, {
1394
1475
  cwd: this.config.rootDir,
1395
1476
  ignore: exclude
1396
1477
  });
@@ -1399,7 +1480,7 @@ var ScreenAnalyzer = class {
1399
1480
  }
1400
1481
  let screenPatternFiles = null;
1401
1482
  if (this.strictScreens) {
1402
- const matched = await (0, import_fast_glob.default)(this.screenPatterns, {
1483
+ const matched = await globSorted(this.screenPatterns, {
1403
1484
  cwd: this.config.rootDir,
1404
1485
  ignore: exclude
1405
1486
  });
@@ -1415,7 +1496,8 @@ var ScreenAnalyzer = class {
1415
1496
  if (this.strictScreens) {
1416
1497
  const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
1417
1498
  const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
1418
- if (!hasRegisterScreen && !matchesPattern) {
1499
+ const isRouteTarget = this.routeTargetFiles.has(filePath);
1500
+ if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
1419
1501
  this.screensFilteredOut++;
1420
1502
  if (this.verbose) {
1421
1503
  console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
@@ -1623,6 +1705,8 @@ var ScreenAnalyzer = class {
1623
1705
  action.riskLevel = value.value;
1624
1706
  } else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
1625
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;
1626
1710
  } else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
1627
1711
  action.appilotsInferred = this.parseAppilotsInferredObject(value);
1628
1712
  }
@@ -2610,43 +2694,880 @@ var ScreenAnalyzer = class {
2610
2694
  // src/analyzers/NavigationAnalyzer.ts
2611
2695
  var import_fs4 = require("fs");
2612
2696
  var import_path4 = __toESM(require("path"));
2613
- var import_fast_glob2 = __toESM(require("fast-glob"));
2614
- var parser = __toESM(require("@babel/parser"));
2697
+ var parser2 = __toESM(require("@babel/parser"));
2615
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"));
2616
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
+ );
2617
3282
  var NavigationAnalyzer = class {
2618
3283
  config;
2619
3284
  /** Extra glob patterns for navigation file discovery (added to defaults) */
2620
3285
  navigationInclude;
2621
3286
  /** Extra glob patterns to exclude from navigation analysis */
2622
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
+ };
2623
3321
  constructor(config, options) {
2624
3322
  this.config = config;
3323
+ this.graph = new ModuleGraph(config.rootDir);
2625
3324
  this.navigationInclude = options?.navigationInclude ?? [];
2626
3325
  this.navigationExclude = options?.navigationExclude ?? [];
2627
3326
  }
2628
3327
  /** Build the full navigation graph */
2629
3328
  async analyze() {
2630
3329
  const navigationFiles = await this.findNavigationFiles();
2631
- const parsedNavigators = [];
2632
3330
  const typeExports = [];
3331
+ this.routeTargets.clear();
3332
+ const declarations = /* @__PURE__ */ new Map();
3333
+ const evidenceFiles = [];
3334
+ this.navigatorFiles.clear();
2633
3335
  for (const filePath of navigationFiles) {
2634
3336
  try {
2635
3337
  const content = await import_fs4.promises.readFile(filePath, "utf-8");
2636
- const navigators = this.parseNavigators(content, filePath);
2637
- const types = this.parseParamTypes(content, filePath);
2638
- parsedNavigators.push(...navigators);
2639
- 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));
2640
3345
  } catch (error2) {
2641
3346
  console.warn(`Failed to parse ${filePath}:`, error2);
2642
3347
  }
2643
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
+ );
2644
3447
  this.attachParamsToNavigators(parsedNavigators, typeExports);
2645
3448
  return this.buildNavigationGraph(parsedNavigators);
2646
3449
  }
2647
- /** 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
+ */
2648
3567
  async findNavigationFiles() {
2649
3568
  const patterns = [
3569
+ ...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
3570
+ // Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
2650
3571
  "**/navigation/**/*.{ts,tsx}",
2651
3572
  "**/navigator*.{ts,tsx}",
2652
3573
  "**/routes*.{ts,tsx}",
@@ -2657,91 +3578,188 @@ var NavigationAnalyzer = class {
2657
3578
  "**/node_modules/**",
2658
3579
  "**/dist/**",
2659
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}",
2660
3598
  ...this.config.exclude || [],
2661
3599
  // §5: Add user-configured navigation excludes
2662
3600
  ...this.navigationExclude
2663
3601
  ];
2664
- const files = await (0, import_fast_glob2.default)(patterns, {
3602
+ const files = await globSorted(patterns, {
2665
3603
  cwd: this.config.rootDir,
2666
3604
  ignore: excludePatterns
2667
3605
  });
2668
3606
  return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
2669
3607
  }
2670
- /** Parse navigator definitions from a file */
2671
- parseNavigators(content, filePath) {
2672
- const navigators = [];
2673
- try {
2674
- const ast = parser.parse(content, {
2675
- sourceType: "module",
2676
- plugins: [
2677
- "jsx",
2678
- "typescript",
2679
- ...this.config.parserPlugins || []
2680
- ]
2681
- });
2682
- const navigatorCalls = /* @__PURE__ */ new Map();
2683
- const screensByNavigator = /* @__PURE__ */ new Map();
2684
- (0, import_traverse5.default)(ast, {
2685
- // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
2686
- CallExpression: (nodePath) => {
2687
- const { node } = nodePath;
2688
- const callee = node.callee;
2689
- let navigatorType = null;
2690
- if (t5.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
2691
- navigatorType = "stack";
2692
- } else if (t5.isIdentifier(callee) && callee.name === "createStackNavigator") {
2693
- navigatorType = "stack";
2694
- } else if (t5.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
2695
- navigatorType = "tab";
2696
- } else if (t5.isIdentifier(callee) && callee.name === "createTabNavigator") {
2697
- navigatorType = "tab";
2698
- } else if (t5.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
2699
- navigatorType = "drawer";
2700
- }
2701
- if (navigatorType) {
2702
- const parent = nodePath.parent;
2703
- if (t5.isVariableDeclarator(parent) && t5.isIdentifier(parent.id)) {
2704
- const varName = parent.id.name;
2705
- navigatorCalls.set(varName, {
2706
- name: varName,
2707
- type: navigatorType,
2708
- screens: []
2709
- });
2710
- }
2711
- }
2712
- },
2713
- // Detect Stack.Navigator / Tab.Navigator JSX elements
2714
- JSXElement: (nodePath) => {
2715
- const { node } = nodePath;
2716
- const openingElement = node.openingElement;
2717
- if (t5.isJSXMemberExpression(openingElement.name) && t5.isJSXIdentifier(openingElement.name.object)) {
2718
- const objectName = openingElement.name.object.name;
2719
- const propertyName = t5.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2720
- if (propertyName === "Navigator") {
2721
- const navigator = navigatorCalls.get(objectName);
2722
- if (navigator) {
2723
- const initialRouteAttr = openingElement.attributes.find(
2724
- (attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2725
- );
2726
- if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
2727
- navigator.initialRouteName = initialRouteAttr.value.value;
2728
- }
2729
- const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
2730
- screensByNavigator.set(objectName, screens);
2731
- }
2732
- }
2733
- }
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;
2734
3622
  }
2735
- });
2736
- navigatorCalls.forEach((navigator) => {
2737
- const screens = screensByNavigator.get(navigator.name) || [];
2738
- navigator.screens = screens;
2739
- navigators.push(navigator);
2740
- });
2741
- } catch (error2) {
2742
- 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);
2743
3761
  }
2744
- return navigators;
3762
+ return null;
2745
3763
  }
2746
3764
  /**
2747
3765
  * Is this JSX element `<navigatorVarName.MEMBER …>`?
@@ -2752,9 +3770,9 @@ var NavigationAnalyzer = class {
2752
3770
  // `never`. It is a question about the member name, not about the node
2753
3771
  // kind.
2754
3772
  isNavigatorMember(node, navigatorVarName, member) {
2755
- if (!t5.isJSXElement(node)) return false;
3773
+ if (!t6.isJSXElement(node)) return false;
2756
3774
  const name = node.openingElement.name;
2757
- 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;
2758
3776
  }
2759
3777
  /**
2760
3778
  * Flatten a navigator's JSX children into the `<X.Screen>` elements they
@@ -2785,37 +3803,37 @@ var NavigationAnalyzer = class {
2785
3803
  found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
2786
3804
  return;
2787
3805
  }
2788
- if (t5.isJSXElement(node)) {
3806
+ if (t6.isJSXElement(node)) {
2789
3807
  const name = node.openingElement.name;
2790
- 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";
2791
3809
  if (isForeignNavigator) return;
2792
3810
  found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2793
3811
  return;
2794
3812
  }
2795
- if (t5.isJSXFragment(node)) {
3813
+ if (t6.isJSXFragment(node)) {
2796
3814
  found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2797
3815
  return;
2798
3816
  }
2799
- if (t5.isJSXExpressionContainer(node)) {
3817
+ if (t6.isJSXExpressionContainer(node)) {
2800
3818
  visit(node.expression);
2801
3819
  return;
2802
3820
  }
2803
- if (t5.isConditionalExpression(node)) {
3821
+ if (t6.isConditionalExpression(node)) {
2804
3822
  visit(node.consequent);
2805
3823
  visit(node.alternate);
2806
3824
  return;
2807
3825
  }
2808
- if (t5.isLogicalExpression(node)) {
3826
+ if (t6.isLogicalExpression(node)) {
2809
3827
  visit(node.left);
2810
3828
  visit(node.right);
2811
3829
  return;
2812
3830
  }
2813
- if (t5.isCallExpression(node)) {
3831
+ if (t6.isCallExpression(node)) {
2814
3832
  for (const arg of node.arguments) {
2815
- if (t5.isArrowFunctionExpression(arg) || t5.isFunctionExpression(arg)) {
2816
- if (t5.isBlockStatement(arg.body)) {
3833
+ if (t6.isArrowFunctionExpression(arg) || t6.isFunctionExpression(arg)) {
3834
+ if (t6.isBlockStatement(arg.body)) {
2817
3835
  for (const stmt of arg.body.body) {
2818
- if (t5.isReturnStatement(stmt)) visit(stmt.argument);
3836
+ if (t6.isReturnStatement(stmt)) visit(stmt.argument);
2819
3837
  }
2820
3838
  } else {
2821
3839
  visit(arg.body);
@@ -2824,11 +3842,11 @@ var NavigationAnalyzer = class {
2824
3842
  }
2825
3843
  return;
2826
3844
  }
2827
- if (t5.isArrayExpression(node)) {
3845
+ if (t6.isArrayExpression(node)) {
2828
3846
  for (const el of node.elements) visit(el);
2829
3847
  return;
2830
3848
  }
2831
- if (t5.isTSAsExpression(node) || t5.isTSNonNullExpression(node)) {
3849
+ if (t6.isTSAsExpression(node) || t6.isTSNonNullExpression(node)) {
2832
3850
  visit(node.expression);
2833
3851
  }
2834
3852
  };
@@ -2836,28 +3854,54 @@ var NavigationAnalyzer = class {
2836
3854
  return found;
2837
3855
  }
2838
3856
  /** Extract screens from a navigator JSX element */
2839
- extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
3857
+ extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
2840
3858
  const screens = [];
2841
3859
  if (!navigatorElement.children) return screens;
2842
3860
  const seen = /* @__PURE__ */ new Set();
2843
3861
  for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
2844
- const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
2845
- if (!screenName || seen.has(screenName)) continue;
2846
- seen.add(screenName);
2847
- screens.push({
2848
- name: screenName,
2849
- navigatorName: navigatorVarName,
2850
- navigatorType
2851
- });
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);
2852
3866
  }
2853
3867
  return screens;
2854
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
+ }
2855
3899
  /** Extract string attribute value from JSX attributes */
2856
3900
  extractAttributeValue(attributes, attrName) {
2857
3901
  const attr = attributes.find(
2858
- (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
2859
3903
  );
2860
- if (t5.isJSXAttribute(attr) && t5.isStringLiteral(attr.value)) {
3904
+ if (t6.isJSXAttribute(attr) && t6.isStringLiteral(attr.value)) {
2861
3905
  return attr.value.value;
2862
3906
  }
2863
3907
  return null;
@@ -2866,7 +3910,7 @@ var NavigationAnalyzer = class {
2866
3910
  parseParamTypes(content, filePath) {
2867
3911
  const types = [];
2868
3912
  try {
2869
- const ast = parser.parse(content, {
3913
+ const ast = parser2.parse(content, {
2870
3914
  sourceType: "module",
2871
3915
  plugins: [
2872
3916
  "jsx",
@@ -2947,7 +3991,7 @@ var NavigationAnalyzer = class {
2947
3991
  if (type.type === "TSUndefinedKeyword") return "undefined";
2948
3992
  if (type.type === "TSNullKeyword") return "null";
2949
3993
  if (type.type === "TSUnionType") {
2950
- return type.types.map((t15) => this.typeToString(t15)).join(" | ");
3994
+ return type.types.map((t16) => this.typeToString(t16)).join(" | ");
2951
3995
  }
2952
3996
  if (type.type === "TSTypeLiteral") {
2953
3997
  return "object";
@@ -2968,7 +4012,7 @@ var NavigationAnalyzer = class {
2968
4012
  /** Attach parsed type params to navigator screens */
2969
4013
  attachParamsToNavigators(navigators, types) {
2970
4014
  for (const navigator of navigators) {
2971
- const matchingType = types.find((t15) => t15.type === navigator.type);
4015
+ const matchingType = types.find((t16) => t16.type === navigator.type);
2972
4016
  if (matchingType) {
2973
4017
  for (const screen of navigator.screens) {
2974
4018
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -3046,9 +4090,9 @@ var NavigationAnalyzer = class {
3046
4090
 
3047
4091
  // src/analyzers/ComponentAnalyzer.ts
3048
4092
  var import_fs5 = require("fs");
3049
- var parser2 = __toESM(require("@babel/parser"));
4093
+ var parser3 = __toESM(require("@babel/parser"));
3050
4094
  var import_traverse6 = __toESM(require("@babel/traverse"));
3051
- var t6 = __toESM(require("@babel/types"));
4095
+ var t7 = __toESM(require("@babel/types"));
3052
4096
  var ComponentAnalyzer = class {
3053
4097
  config;
3054
4098
  constructor(config) {
@@ -3058,14 +4102,14 @@ var ComponentAnalyzer = class {
3058
4102
  async analyzeFile(filePath) {
3059
4103
  try {
3060
4104
  const code = (0, import_fs5.readFileSync)(filePath, "utf-8");
3061
- const ast = parser2.parse(code, {
4105
+ const ast = parser3.parse(code, {
3062
4106
  sourceType: "module",
3063
4107
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
3064
4108
  });
3065
4109
  const components = [];
3066
4110
  (0, import_traverse6.default)(ast, {
3067
- JSXElement: (path11) => {
3068
- const component = this.extractComponentFromJSXElement(path11.node);
4111
+ JSXElement: (path13) => {
4112
+ const component = this.extractComponentFromJSXElement(path13.node);
3069
4113
  if (component) {
3070
4114
  components.push(component);
3071
4115
  }
@@ -3094,19 +4138,19 @@ var ComponentAnalyzer = class {
3094
4138
  };
3095
4139
  }
3096
4140
  getElementName(openingElement) {
3097
- if (t6.isJSXIdentifier(openingElement.name)) {
4141
+ if (t7.isJSXIdentifier(openingElement.name)) {
3098
4142
  return openingElement.name.name;
3099
4143
  }
3100
- if (t6.isJSXMemberExpression(openingElement.name)) {
4144
+ if (t7.isJSXMemberExpression(openingElement.name)) {
3101
4145
  const parts = [];
3102
4146
  let current = openingElement.name;
3103
- while (t6.isJSXMemberExpression(current)) {
3104
- if (t6.isJSXIdentifier(current.property)) {
4147
+ while (t7.isJSXMemberExpression(current)) {
4148
+ if (t7.isJSXIdentifier(current.property)) {
3105
4149
  parts.unshift(current.property.name);
3106
4150
  }
3107
4151
  current = current.object;
3108
4152
  }
3109
- if (t6.isJSXIdentifier(current)) {
4153
+ if (t7.isJSXIdentifier(current)) {
3110
4154
  parts.unshift(current.name);
3111
4155
  }
3112
4156
  return parts.join(".");
@@ -3129,13 +4173,13 @@ var ComponentAnalyzer = class {
3129
4173
  extractProps(openingElement) {
3130
4174
  const props = {};
3131
4175
  for (const attr of openingElement.attributes) {
3132
- if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
4176
+ if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
3133
4177
  const propName = attr.name.name;
3134
4178
  if (attr.value === null) {
3135
4179
  props[propName] = "true";
3136
- } else if (t6.isStringLiteral(attr.value)) {
4180
+ } else if (t7.isStringLiteral(attr.value)) {
3137
4181
  props[propName] = attr.value.value;
3138
- } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
4182
+ } else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
3139
4183
  props[propName] = attr.value.expression.value;
3140
4184
  }
3141
4185
  }
@@ -3145,12 +4189,12 @@ var ComponentAnalyzer = class {
3145
4189
  extractAccessibilityProps(openingElement) {
3146
4190
  const result = {};
3147
4191
  for (const attr of openingElement.attributes) {
3148
- if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
4192
+ if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
3149
4193
  const propName = attr.name.name;
3150
4194
  if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
3151
- if (t6.isStringLiteral(attr.value)) {
4195
+ if (t7.isStringLiteral(attr.value)) {
3152
4196
  result[propName] = attr.value.value;
3153
- } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
4197
+ } else if (t7.isJSXExpressionContainer(attr.value) && t7.isStringLiteral(attr.value.expression)) {
3154
4198
  result[propName] = attr.value.expression.value;
3155
4199
  }
3156
4200
  }
@@ -3162,7 +4206,7 @@ var ComponentAnalyzer = class {
3162
4206
  if (depth >= maxDepth) return [];
3163
4207
  const extracted = [];
3164
4208
  for (const child of children) {
3165
- if (t6.isJSXElement(child)) {
4209
+ if (t7.isJSXElement(child)) {
3166
4210
  const component = this.extractComponentFromJSXElement(child);
3167
4211
  if (component) {
3168
4212
  extracted.push(component);
@@ -3175,10 +4219,10 @@ var ComponentAnalyzer = class {
3175
4219
 
3176
4220
  // src/analyzers/FormAnalyzer.ts
3177
4221
  var import_fs6 = require("fs");
3178
- var parser3 = __toESM(require("@babel/parser"));
4222
+ var parser4 = __toESM(require("@babel/parser"));
3179
4223
  var import_traverse7 = __toESM(require("@babel/traverse"));
3180
- var t7 = __toESM(require("@babel/types"));
3181
- var path3 = __toESM(require("path"));
4224
+ var t8 = __toESM(require("@babel/types"));
4225
+ var path5 = __toESM(require("path"));
3182
4226
  var FormAnalyzer = class {
3183
4227
  config;
3184
4228
  stateVariables = /* @__PURE__ */ new Map();
@@ -3191,7 +4235,7 @@ var FormAnalyzer = class {
3191
4235
  async analyzeFile(filePath) {
3192
4236
  try {
3193
4237
  const code = (0, import_fs6.readFileSync)(filePath, "utf-8");
3194
- const ast = parser3.parse(code, {
4238
+ const ast = parser4.parse(code, {
3195
4239
  sourceType: "module",
3196
4240
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
3197
4241
  });
@@ -3199,13 +4243,13 @@ var FormAnalyzer = class {
3199
4243
  this.inputElements = [];
3200
4244
  this.submitButtons = [];
3201
4245
  (0, import_traverse7.default)(ast, {
3202
- CallExpression: (path11) => {
3203
- this.extractStateVariables(path11.node);
4246
+ CallExpression: (path13) => {
4247
+ this.extractStateVariables(path13.node);
3204
4248
  }
3205
4249
  });
3206
4250
  (0, import_traverse7.default)(ast, {
3207
- JSXElement: (path11) => {
3208
- this.extractFormElements(path11.node);
4251
+ JSXElement: (path13) => {
4252
+ this.extractFormElements(path13.node);
3209
4253
  }
3210
4254
  });
3211
4255
  const validationRules = this.extractValidationRules(ast);
@@ -3216,7 +4260,7 @@ var FormAnalyzer = class {
3216
4260
  }
3217
4261
  }
3218
4262
  extractStateVariables(node) {
3219
- 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) {
3220
4264
  return;
3221
4265
  }
3222
4266
  }
@@ -3237,7 +4281,7 @@ var FormAnalyzer = class {
3237
4281
  extractInputInfo(openingElement) {
3238
4282
  const info2 = { varName: "" };
3239
4283
  for (const attr of openingElement.attributes) {
3240
- if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
4284
+ if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
3241
4285
  const propName = attr.name.name;
3242
4286
  const propValue = this.extractAttributeValue(attr.value);
3243
4287
  switch (propName) {
@@ -3258,7 +4302,7 @@ var FormAnalyzer = class {
3258
4302
  if (propValue) info2.varName = propValue;
3259
4303
  break;
3260
4304
  case "value":
3261
- 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)) {
3262
4306
  info2.varName = attr.value.expression.name;
3263
4307
  }
3264
4308
  break;
@@ -3270,16 +4314,16 @@ var FormAnalyzer = class {
3270
4314
  extractButtonInfo(openingElement) {
3271
4315
  const info2 = {};
3272
4316
  for (const attr of openingElement.attributes) {
3273
- if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
4317
+ if (t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name)) {
3274
4318
  const propName = attr.name.name;
3275
4319
  if (propName === "title" || propName === "label") {
3276
4320
  info2.label = this.extractAttributeValue(attr.value);
3277
4321
  }
3278
4322
  if (propName === "onPress") {
3279
- if (attr.value && t7.isJSXExpressionContainer(attr.value)) {
3280
- if (t7.isIdentifier(attr.value.expression)) {
4323
+ if (attr.value && t8.isJSXExpressionContainer(attr.value)) {
4324
+ if (t8.isIdentifier(attr.value.expression)) {
3281
4325
  info2.handler = attr.value.expression.name;
3282
- } 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)) {
3283
4327
  info2.handler = "anonymous";
3284
4328
  }
3285
4329
  }
@@ -3290,14 +4334,14 @@ var FormAnalyzer = class {
3290
4334
  }
3291
4335
  extractAttributeValue(value) {
3292
4336
  if (value === null) return void 0;
3293
- if (t7.isStringLiteral(value)) return value.value;
3294
- 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)) {
3295
4339
  return value.expression.value;
3296
4340
  }
3297
4341
  return void 0;
3298
4342
  }
3299
4343
  getElementName(openingElement) {
3300
- if (t7.isJSXIdentifier(openingElement.name)) {
4344
+ if (t8.isJSXIdentifier(openingElement.name)) {
3301
4345
  return openingElement.name.name;
3302
4346
  }
3303
4347
  return null;
@@ -3305,8 +4349,8 @@ var FormAnalyzer = class {
3305
4349
  extractValidationRules(ast) {
3306
4350
  const rules = {};
3307
4351
  (0, import_traverse7.default)(ast, {
3308
- IfStatement: (path11) => {
3309
- const test = path11.node.test;
4352
+ IfStatement: (path13) => {
4353
+ const test = path13.node.test;
3310
4354
  const rule = this.extractRuleFromCondition(test);
3311
4355
  if (rule) {
3312
4356
  const { field, description } = rule;
@@ -3321,36 +4365,36 @@ var FormAnalyzer = class {
3321
4365
  extractRuleFromCondition(test) {
3322
4366
  let fieldName = "";
3323
4367
  let description = "";
3324
- if (t7.isUnaryExpression(test) && test.operator === "!") {
3325
- if (t7.isCallExpression(test.argument)) {
4368
+ if (t8.isUnaryExpression(test) && test.operator === "!") {
4369
+ if (t8.isCallExpression(test.argument)) {
3326
4370
  const callExpr = test.argument;
3327
- if (t7.isMemberExpression(callExpr.callee)) {
4371
+ if (t8.isMemberExpression(callExpr.callee)) {
3328
4372
  const memberExpr = callExpr.callee;
3329
- if (t7.isIdentifier(memberExpr.object)) {
4373
+ if (t8.isIdentifier(memberExpr.object)) {
3330
4374
  fieldName = memberExpr.object.name;
3331
4375
  description = `${fieldName} is required`;
3332
4376
  }
3333
4377
  }
3334
- } else if (t7.isIdentifier(test.argument)) {
4378
+ } else if (t8.isIdentifier(test.argument)) {
3335
4379
  fieldName = test.argument.name;
3336
4380
  description = `${fieldName} is required`;
3337
4381
  }
3338
4382
  }
3339
- if (t7.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
3340
- if (t7.isMemberExpression(test.left)) {
4383
+ if (t8.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
4384
+ if (t8.isMemberExpression(test.left)) {
3341
4385
  const memberExpr = test.left;
3342
- if (t7.isIdentifier(memberExpr.object)) {
4386
+ if (t8.isIdentifier(memberExpr.object)) {
3343
4387
  fieldName = memberExpr.object.name;
3344
- if (t7.isNumericLiteral(test.right)) {
4388
+ if (t8.isNumericLiteral(test.right)) {
3345
4389
  description = `${fieldName} must be at least ${test.right.value} characters`;
3346
4390
  }
3347
4391
  }
3348
4392
  }
3349
4393
  }
3350
- if (t7.isUnaryExpression(test) && test.operator === "!") {
3351
- if (t7.isCallExpression(test.argument) && t7.isMemberExpression(test.argument.callee)) {
3352
- const methodName = t7.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
3353
- 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)) {
3354
4398
  fieldName = test.argument.callee.object.name;
3355
4399
  description = `${fieldName} format is invalid`;
3356
4400
  }
@@ -3360,7 +4404,7 @@ var FormAnalyzer = class {
3360
4404
  }
3361
4405
  buildForms(filePath, validationRules) {
3362
4406
  if (this.inputElements.length === 0) return [];
3363
- const fileName = path3.basename(filePath, path3.extname(filePath));
4407
+ const fileName = path5.basename(filePath, path5.extname(filePath));
3364
4408
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
3365
4409
  const fields = this.inputElements.map((input) => {
3366
4410
  const fieldType = this.inferFieldType(input);
@@ -3410,14 +4454,104 @@ var FormAnalyzer = class {
3410
4454
  }
3411
4455
  };
3412
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
+
3413
4551
  // src/analyzers/ReactNativePlatformAnalyzer.ts
3414
4552
  var ReactNativePlatformAnalyzer = class {
3415
4553
  platform = "react-native";
3416
4554
  async analyze(config, options) {
3417
- const screenAnalyzer = new ScreenAnalyzer(config, {
3418
- strictScreens: options.strictScreens ?? true,
3419
- screenPatterns: options.screenPatterns
3420
- });
3421
4555
  const navigationAnalyzer = new NavigationAnalyzer(config, {
3422
4556
  navigationInclude: options.navigationInclude,
3423
4557
  navigationExclude: options.navigationExclude
@@ -3425,14 +4559,31 @@ var ReactNativePlatformAnalyzer = class {
3425
4559
  const componentAnalyzer = new ComponentAnalyzer(config);
3426
4560
  const formAnalyzer = new FormAnalyzer(config);
3427
4561
  console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
3428
- const [screens, navigation] = await Promise.all([
3429
- screenAnalyzer.analyze(),
3430
- navigationAnalyzer.analyze()
3431
- ]);
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
+ );
3432
4583
  console.log(
3433
4584
  `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
3434
4585
  );
3435
- const screenFiles = await (0, import_fast_glob3.default)(config.include || ["**/*.tsx", "**/*.ts"], {
4586
+ const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
3436
4587
  cwd: config.rootDir,
3437
4588
  ignore: config.exclude || ["**/node_modules/**"]
3438
4589
  });
@@ -3440,7 +4591,7 @@ var ReactNativePlatformAnalyzer = class {
3440
4591
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
3441
4592
  );
3442
4593
  const enrichmentPromises = screenFiles.map(async (file2) => {
3443
- const filePath = import_node_path.default.resolve(config.rootDir, file2);
4594
+ const filePath = import_node_path3.default.resolve(config.rootDir, file2);
3444
4595
  try {
3445
4596
  const [components, forms] = await Promise.all([
3446
4597
  componentAnalyzer.analyzeFile(filePath),
@@ -3495,7 +4646,8 @@ var ReactNativePlatformAnalyzer = class {
3495
4646
  screens: enrichedScreens,
3496
4647
  navigation,
3497
4648
  analyzedFiles: screenFiles.length,
3498
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
4649
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
4650
+ diagnostics: { navigation: navigationAnalyzer.diagnostics }
3499
4651
  };
3500
4652
  }
3501
4653
  mergeForm(target, source) {
@@ -3513,9 +4665,12 @@ var ReactNativePlatformAnalyzer = class {
3513
4665
  findEquivalentField(fields, incoming) {
3514
4666
  return fields.find((field) => {
3515
4667
  if (field.name && incoming.name && field.name === incoming.name) return true;
3516
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
3517
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
3518
- 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;
3519
4674
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
3520
4675
  return true;
3521
4676
  }
@@ -3548,7 +4703,9 @@ var ReactNativePlatformAnalyzer = class {
3548
4703
  if (incoming.fields.length === 0) return void 0;
3549
4704
  let best;
3550
4705
  for (const form of forms) {
3551
- 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;
3552
4709
  if (overlap > 0 && (!best || overlap > best.overlap)) {
3553
4710
  best = { form, overlap };
3554
4711
  }
@@ -3575,9 +4732,8 @@ var GenericPlatformAnalyzer = class {
3575
4732
  // src/analyzers/web/WebScreenAnalyzer.ts
3576
4733
  var import_promises2 = __toESM(require("fs/promises"));
3577
4734
  var import_path5 = __toESM(require("path"));
3578
- var import_fast_glob4 = __toESM(require("fast-glob"));
3579
4735
  var import_traverse9 = __toESM(require("@babel/traverse"));
3580
- var t10 = __toESM(require("@babel/types"));
4736
+ var t11 = __toESM(require("@babel/types"));
3581
4737
 
3582
4738
  // src/ast/jsx/web/classify.ts
3583
4739
  var VIEW_TAGS = /* @__PURE__ */ new Set([
@@ -3693,16 +4849,16 @@ function classifyWebJsxComponent(name, element) {
3693
4849
  }
3694
4850
 
3695
4851
  // src/ast/jsx/names.ts
3696
- var t8 = __toESM(require("@babel/types"));
4852
+ var t9 = __toESM(require("@babel/types"));
3697
4853
  function getJsxElementName(openingElement) {
3698
4854
  return getJsxName(openingElement.name);
3699
4855
  }
3700
4856
  function getJsxName(name) {
3701
- if (t8.isJSXIdentifier(name)) return name.name;
3702
- if (t8.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
3703
- 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)) {
3704
4860
  const objectName = getJsxName(name.object);
3705
- const propertyName = t8.isJSXIdentifier(name.property) ? name.property.name : null;
4861
+ const propertyName = t9.isJSXIdentifier(name.property) ? name.property.name : null;
3706
4862
  return objectName && propertyName ? `${objectName}.${propertyName}` : null;
3707
4863
  }
3708
4864
  return null;
@@ -3710,32 +4866,32 @@ function getJsxName(name) {
3710
4866
 
3711
4867
  // src/ast/navigation/web-calls.ts
3712
4868
  var import_traverse8 = __toESM(require("@babel/traverse"));
3713
- var t9 = __toESM(require("@babel/types"));
4869
+ var t10 = __toESM(require("@babel/types"));
3714
4870
  var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
3715
4871
  function staticRoutePath(node) {
3716
4872
  if (!node) return void 0;
3717
- if (t9.isStringLiteral(node)) return node.value;
3718
- if (t9.isTemplateLiteral(node)) {
3719
- let path11 = "";
4873
+ if (t10.isStringLiteral(node)) return node.value;
4874
+ if (t10.isTemplateLiteral(node)) {
4875
+ let path13 = "";
3720
4876
  node.quasis.forEach((quasi, index) => {
3721
- path11 += quasi.value.cooked ?? quasi.value.raw;
4877
+ path13 += quasi.value.cooked ?? quasi.value.raw;
3722
4878
  const expr = node.expressions[index];
3723
- if (expr) path11 += `:${paramNameOf(expr)}`;
4879
+ if (expr) path13 += `:${paramNameOf(expr)}`;
3724
4880
  });
3725
- return path11;
4881
+ return path13;
3726
4882
  }
3727
4883
  return void 0;
3728
4884
  }
3729
4885
  function paramNameOf(expr) {
3730
- if (t9.isIdentifier(expr)) return expr.name;
3731
- 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;
3732
4888
  return "param";
3733
4889
  }
3734
4890
  function extractWebNavigationCalls(ast) {
3735
4891
  const calls = [];
3736
4892
  const inspect = (node) => {
3737
- if (t9.isCallExpression(node)) {
3738
- if (t9.isIdentifier(node.callee) && node.callee.name === "navigate") {
4893
+ if (t10.isCallExpression(node)) {
4894
+ if (t10.isIdentifier(node.callee) && node.callee.name === "navigate") {
3739
4895
  const first = node.arguments[0];
3740
4896
  const targetPath = staticRoutePath(first);
3741
4897
  if (targetPath !== void 0) {
@@ -3743,12 +4899,12 @@ function extractWebNavigationCalls(ast) {
3743
4899
  method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
3744
4900
  targetPath
3745
4901
  });
3746
- } else if (t9.isNumericLiteral(first) || t9.isUnaryExpression(first) && first.operator === "-") {
4902
+ } else if (t10.isNumericLiteral(first) || t10.isUnaryExpression(first) && first.operator === "-") {
3747
4903
  calls.push({ method: "goBack" });
3748
4904
  }
3749
4905
  return;
3750
4906
  }
3751
- 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)) {
3752
4908
  const method = node.callee.property.name;
3753
4909
  const targetPath = staticRoutePath(node.arguments[0]);
3754
4910
  if ((method === "push" || method === "navigate") && targetPath !== void 0) {
@@ -3761,25 +4917,25 @@ function extractWebNavigationCalls(ast) {
3761
4917
  }
3762
4918
  return;
3763
4919
  }
3764
- 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("/")) {
3765
4921
  calls.push({ method: "navigate", targetPath: node.right.value });
3766
4922
  }
3767
4923
  };
3768
4924
  (0, import_traverse8.default)(ast, {
3769
- noScope: !t9.isFile(ast),
4925
+ noScope: !t10.isFile(ast),
3770
4926
  enter: (nodePath) => inspect(nodePath.node)
3771
4927
  });
3772
4928
  return calls;
3773
4929
  }
3774
4930
  function hasReplaceOption(arg) {
3775
- if (!arg || !t9.isObjectExpression(arg)) return false;
4931
+ if (!arg || !t10.isObjectExpression(arg)) return false;
3776
4932
  return arg.properties.some(
3777
- (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
3778
4934
  );
3779
4935
  }
3780
4936
  function isLocationExpression(node) {
3781
- if (t9.isIdentifier(node)) return node.name === "location";
3782
- 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";
3783
4939
  }
3784
4940
 
3785
4941
  // src/analyzers/web/WebScreenAnalyzer.ts
@@ -3807,10 +4963,10 @@ var WebScreenAnalyzer = class {
3807
4963
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
3808
4964
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
3809
4965
  } = this.config;
3810
- 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(
3811
4967
  (file2) => !file2.endsWith(".d.ts")
3812
4968
  );
3813
- const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
4969
+ const patternMatches = await globSorted(this.screenPatterns, {
3814
4970
  cwd: this.config.rootDir,
3815
4971
  ignore: exclude
3816
4972
  });
@@ -3904,7 +5060,7 @@ var WebScreenAnalyzer = class {
3904
5060
  CallExpression: (nodePath) => {
3905
5061
  if (!isRegisterScreenCallee(nodePath.node.callee)) return;
3906
5062
  const arg = nodePath.node.arguments[0];
3907
- if (t10.isObjectExpression(arg)) {
5063
+ if (t11.isObjectExpression(arg)) {
3908
5064
  plain = literalToPlain(arg);
3909
5065
  }
3910
5066
  }
@@ -3951,20 +5107,20 @@ var WebScreenAnalyzer = class {
3951
5107
  (0, import_traverse9.default)(ast, {
3952
5108
  ExportDefaultDeclaration: (nodePath) => {
3953
5109
  const declaration = nodePath.node.declaration;
3954
- if (t10.isFunctionDeclaration(declaration) && declaration.id?.name) {
5110
+ if (t11.isFunctionDeclaration(declaration) && declaration.id?.name) {
3955
5111
  defaultName = declaration.id.name;
3956
- } else if (t10.isIdentifier(declaration)) {
5112
+ } else if (t11.isIdentifier(declaration)) {
3957
5113
  defaultName = declaration.name;
3958
5114
  }
3959
5115
  },
3960
5116
  ExportNamedDeclaration: (nodePath) => {
3961
5117
  if (firstExported) return;
3962
5118
  const declaration = nodePath.node.declaration;
3963
- 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)) {
3964
5120
  firstExported = declaration.id.name;
3965
- } else if (t10.isVariableDeclaration(declaration)) {
5121
+ } else if (t11.isVariableDeclaration(declaration)) {
3966
5122
  for (const declarator of declaration.declarations) {
3967
- 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))) {
3968
5124
  firstExported = declarator.id.name;
3969
5125
  break;
3970
5126
  }
@@ -4136,15 +5292,15 @@ var WebScreenAnalyzer = class {
4136
5292
  });
4137
5293
  }
4138
5294
  for (const attr of opening.attributes) {
4139
- if (!t10.isJSXAttribute(attr) || !t10.isJSXIdentifier(attr.name)) continue;
5295
+ if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
4140
5296
  const attrName = attr.name.name;
4141
5297
  if (attrName === "required") {
4142
5298
  if (attr.value === null) field.required = true;
4143
- else if (t10.isJSXExpressionContainer(attr.value) && t10.isBooleanLiteral(attr.value.expression)) {
5299
+ else if (t11.isJSXExpressionContainer(attr.value) && t11.isBooleanLiteral(attr.value.expression)) {
4144
5300
  field.required = attr.value.expression.value;
4145
5301
  }
4146
5302
  }
4147
- 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)) {
4148
5304
  field.valueBinding = attr.value.expression.name;
4149
5305
  if (!field.name) field.name = attr.value.expression.name;
4150
5306
  }
@@ -4194,7 +5350,7 @@ var WebScreenAnalyzer = class {
4194
5350
  extractSelectOptions(selectElement) {
4195
5351
  const options = [];
4196
5352
  for (const child of selectElement.children) {
4197
- if (!t10.isJSXElement(child)) continue;
5353
+ if (!t11.isJSXElement(child)) continue;
4198
5354
  if (getJsxElementName(child.openingElement) !== "option") continue;
4199
5355
  const value = getStringAttr(child.openingElement, "value");
4200
5356
  const label = jsxTextContent(child) || value || "";
@@ -4305,14 +5461,14 @@ var WebScreenAnalyzer = class {
4305
5461
  }
4306
5462
  let handlerName;
4307
5463
  const onClickAttr = opening.attributes.find(
4308
- (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"
4309
5465
  );
4310
- if (onClickAttr?.value && t10.isJSXExpressionContainer(onClickAttr.value)) {
5466
+ if (onClickAttr?.value && t11.isJSXExpressionContainer(onClickAttr.value)) {
4311
5467
  const expr = onClickAttr.value.expression;
4312
- if (t10.isIdentifier(expr)) {
5468
+ if (t11.isIdentifier(expr)) {
4313
5469
  handlerName = expr.name;
4314
5470
  action.handler = expr.name;
4315
- } else if (!t10.isJSXEmptyExpression(expr)) {
5471
+ } else if (!t11.isJSXEmptyExpression(expr)) {
4316
5472
  const inlineNavCalls = extractWebNavigationCalls(expr);
4317
5473
  const inlineNav = inlineNavCalls.find((call) => call.targetPath);
4318
5474
  if (inlineNav?.targetPath) {
@@ -4451,12 +5607,12 @@ var WebScreenAnalyzer = class {
4451
5607
  }
4452
5608
  handlerNameFromAttr(opening, attrName) {
4453
5609
  const attr = opening.attributes.find(
4454
- (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
4455
5611
  );
4456
- if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
5612
+ if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
4457
5613
  const expr = attr.value.expression;
4458
- if (t10.isIdentifier(expr)) return expr.name;
4459
- if (t10.isArrowFunctionExpression(expr) || t10.isFunctionExpression(expr)) {
5614
+ if (t11.isIdentifier(expr)) return expr.name;
5615
+ if (t11.isArrowFunctionExpression(expr) || t11.isFunctionExpression(expr)) {
4460
5616
  return firstCalledFunctionName(expr) ?? "anonymous";
4461
5617
  }
4462
5618
  return void 0;
@@ -4490,11 +5646,11 @@ var WebScreenAnalyzer = class {
4490
5646
  (0, import_traverse9.default)(ast, {
4491
5647
  JSXExpressionContainer: (nodePath) => {
4492
5648
  const expr = nodePath.node.expression;
4493
- 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") {
4494
5650
  return;
4495
5651
  }
4496
5652
  const callback = expr.arguments[0];
4497
- if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
5653
+ if (!t11.isArrowFunctionExpression(callback) && !t11.isFunctionExpression(callback)) return;
4498
5654
  const enclosing = nodePath.findParent(
4499
5655
  (p) => p.isJSXElement()
4500
5656
  );
@@ -4529,22 +5685,22 @@ var WebScreenAnalyzer = class {
4529
5685
  }
4530
5686
  };
4531
5687
  function isRegisterScreenCallee(callee) {
4532
- 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";
4533
5689
  }
4534
5690
  function literalToPlain(node) {
4535
- if (t10.isStringLiteral(node) || t10.isNumericLiteral(node) || t10.isBooleanLiteral(node)) {
5691
+ if (t11.isStringLiteral(node) || t11.isNumericLiteral(node) || t11.isBooleanLiteral(node)) {
4536
5692
  return node.value;
4537
5693
  }
4538
- if (t10.isNullLiteral(node)) return null;
4539
- if (t10.isArrayExpression(node)) {
4540
- 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);
4541
5697
  }
4542
- if (t10.isObjectExpression(node)) {
5698
+ if (t11.isObjectExpression(node)) {
4543
5699
  const out = {};
4544
5700
  for (const prop of node.properties) {
4545
- if (!t10.isObjectProperty(prop)) continue;
4546
- const key = t10.isIdentifier(prop.key) ? prop.key.name : t10.isStringLiteral(prop.key) ? prop.key.value : void 0;
4547
- 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;
4548
5704
  const value = literalToPlain(prop.value);
4549
5705
  if (value !== void 0) out[key] = value;
4550
5706
  }
@@ -4556,12 +5712,12 @@ function jsxTextContent(element) {
4556
5712
  const parts = [];
4557
5713
  const walk = (children) => {
4558
5714
  for (const child of children) {
4559
- if (t10.isJSXText(child)) {
5715
+ if (t11.isJSXText(child)) {
4560
5716
  const trimmed = child.value.replace(/\s+/g, " ").trim();
4561
5717
  if (trimmed) parts.push(trimmed);
4562
- } else if (t10.isJSXExpressionContainer(child) && t10.isStringLiteral(child.expression)) {
5718
+ } else if (t11.isJSXExpressionContainer(child) && t11.isStringLiteral(child.expression)) {
4563
5719
  parts.push(child.expression.value);
4564
- } else if (t10.isJSXElement(child)) {
5720
+ } else if (t11.isJSXElement(child)) {
4565
5721
  walk(child.children);
4566
5722
  }
4567
5723
  }
@@ -4571,26 +5727,26 @@ function jsxTextContent(element) {
4571
5727
  return text.length > 0 ? text : void 0;
4572
5728
  }
4573
5729
  function firstCalledFunctionName(node) {
4574
- if (t10.isIdentifier(node)) return node.name;
4575
- if (t10.isArrowFunctionExpression(node) || t10.isFunctionExpression(node)) {
5730
+ if (t11.isIdentifier(node)) return node.name;
5731
+ if (t11.isArrowFunctionExpression(node) || t11.isFunctionExpression(node)) {
4576
5732
  return firstCalledFunctionName(node.body);
4577
5733
  }
4578
- if (t10.isBlockStatement(node)) {
5734
+ if (t11.isBlockStatement(node)) {
4579
5735
  for (const statement of node.body) {
4580
5736
  const handler = firstCalledFunctionName(statement);
4581
5737
  if (handler) return handler;
4582
5738
  }
4583
5739
  return void 0;
4584
5740
  }
4585
- if (t10.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
4586
- if (t10.isReturnStatement(node)) {
5741
+ if (t11.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
5742
+ if (t11.isReturnStatement(node)) {
4587
5743
  return node.argument ? firstCalledFunctionName(node.argument) : void 0;
4588
5744
  }
4589
- if (t10.isAwaitExpression(node) || t10.isUnaryExpression(node)) {
5745
+ if (t11.isAwaitExpression(node) || t11.isUnaryExpression(node)) {
4590
5746
  return firstCalledFunctionName(node.argument);
4591
5747
  }
4592
- if (t10.isCallExpression(node)) {
4593
- 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)) {
4594
5750
  return node.callee.name;
4595
5751
  }
4596
5752
  return void 0;
@@ -4605,8 +5761,8 @@ function containsWindowConfirm(body) {
4605
5761
  noScope: true,
4606
5762
  CallExpression: (nodePath) => {
4607
5763
  const callee = nodePath.node.callee;
4608
- if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
4609
- 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") {
4610
5766
  found = true;
4611
5767
  }
4612
5768
  }
@@ -4621,9 +5777,9 @@ function routePathAttr(opening, attrName) {
4621
5777
  const literal = getStringAttr(opening, attrName);
4622
5778
  if (literal) return literal;
4623
5779
  const attr = opening.attributes.find(
4624
- (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
4625
5781
  );
4626
- if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
5782
+ if (!attr?.value || !t11.isJSXExpressionContainer(attr.value)) return void 0;
4627
5783
  return staticRoutePath(attr.value.expression);
4628
5784
  }
4629
5785
  function slugify(label) {
@@ -4638,10 +5794,10 @@ function isWeakInferredFieldName(name) {
4638
5794
  function collectionItemNames(callback) {
4639
5795
  const names = /* @__PURE__ */ new Set(["item"]);
4640
5796
  const firstParam = callback.params[0];
4641
- if (t10.isIdentifier(firstParam)) names.add(firstParam.name);
4642
- if (t10.isObjectPattern(firstParam)) {
5797
+ if (t11.isIdentifier(firstParam)) names.add(firstParam.name);
5798
+ if (t11.isObjectPattern(firstParam)) {
4643
5799
  for (const prop of firstParam.properties) {
4644
- 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)) {
4645
5801
  names.add(prop.value.name);
4646
5802
  }
4647
5803
  }
@@ -4657,7 +5813,7 @@ function collectionDisplayFields(callback, itemNames) {
4657
5813
  noScope: true,
4658
5814
  MemberExpression: (nodePath) => {
4659
5815
  const node = nodePath.node;
4660
- 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)) {
4661
5817
  fields.add(node.property.name);
4662
5818
  }
4663
5819
  }
@@ -4674,10 +5830,10 @@ function collectionKeyField(callback, itemNames) {
4674
5830
  noScope: true,
4675
5831
  JSXAttribute: (nodePath) => {
4676
5832
  const attr = nodePath.node;
4677
- if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
4678
- 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;
4679
5835
  const expr = attr.value.expression;
4680
- 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)) {
4681
5837
  keyField = keyField ?? expr.property.name;
4682
5838
  }
4683
5839
  }
@@ -4700,18 +5856,18 @@ function callbackReturnsTag(callback, tags) {
4700
5856
  let found = false;
4701
5857
  const inspect = (node) => {
4702
5858
  if (!node || found) return;
4703
- if (t10.isJSXElement(node)) {
5859
+ if (t11.isJSXElement(node)) {
4704
5860
  const name = getJsxElementName(node.openingElement);
4705
5861
  if (name && tags.has(name)) found = true;
4706
5862
  return;
4707
5863
  }
4708
- if (t10.isBlockStatement(node)) {
5864
+ if (t11.isBlockStatement(node)) {
4709
5865
  for (const statement of node.body) {
4710
- if (t10.isReturnStatement(statement)) inspect(statement.argument);
5866
+ if (t11.isReturnStatement(statement)) inspect(statement.argument);
4711
5867
  }
4712
5868
  }
4713
- if (t10.isParenthesizedExpression(node)) inspect(node.expression);
4714
- if (t10.isConditionalExpression(node)) {
5869
+ if (t11.isParenthesizedExpression(node)) inspect(node.expression);
5870
+ if (t11.isConditionalExpression(node)) {
4715
5871
  inspect(node.consequent);
4716
5872
  inspect(node.alternate);
4717
5873
  }
@@ -4738,9 +5894,8 @@ function inferIdentityFields(keyField, displayFields) {
4738
5894
  // src/analyzers/web/WebNavigationAnalyzer.ts
4739
5895
  var import_fs7 = require("fs");
4740
5896
  var import_path6 = __toESM(require("path"));
4741
- var import_fast_glob5 = __toESM(require("fast-glob"));
4742
5897
  var import_traverse10 = __toESM(require("@babel/traverse"));
4743
- var t11 = __toESM(require("@babel/types"));
5898
+ var t12 = __toESM(require("@babel/types"));
4744
5899
  var WEB_NAVIGATOR_TYPE = "route";
4745
5900
  var WebNavigationAnalyzer = class {
4746
5901
  config;
@@ -4757,7 +5912,9 @@ var WebNavigationAnalyzer = class {
4757
5912
  for (const filePath of files) {
4758
5913
  try {
4759
5914
  const content = await import_fs7.promises.readFile(filePath, "utf-8");
4760
- if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
5915
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
5916
+ content
5917
+ )) {
4761
5918
  continue;
4762
5919
  }
4763
5920
  const ast = parseSource(content, this.config.parserPlugins);
@@ -4787,7 +5944,7 @@ var WebNavigationAnalyzer = class {
4787
5944
  ...this.config.exclude || [],
4788
5945
  ...this.navigationExclude
4789
5946
  ];
4790
- 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 });
4791
5948
  return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
4792
5949
  }
4793
5950
  // ── JSX <Route> style ────────────────────────────────────────────
@@ -4798,7 +5955,7 @@ var WebNavigationAnalyzer = class {
4798
5955
  const name = getJsxElementName(opening);
4799
5956
  if (name !== "Route") {
4800
5957
  for (const child of element.children) {
4801
- if (t11.isJSXElement(child)) visitRoute(child, parentPath);
5958
+ if (t12.isJSXElement(child)) visitRoute(child, parentPath);
4802
5959
  }
4803
5960
  return;
4804
5961
  }
@@ -4807,13 +5964,13 @@ var WebNavigationAnalyzer = class {
4807
5964
  const fullPath = this.joinPaths(parentPath, segment, isIndex);
4808
5965
  const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
4809
5966
  const isLeaf = !element.children.some(
4810
- (child) => t11.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
5967
+ (child) => t12.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
4811
5968
  );
4812
5969
  if ((segment || isIndex) && (componentName || isLeaf)) {
4813
5970
  routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
4814
5971
  }
4815
5972
  for (const child of element.children) {
4816
- if (t11.isJSXElement(child)) visitRoute(child, fullPath);
5973
+ if (t12.isJSXElement(child)) visitRoute(child, fullPath);
4817
5974
  }
4818
5975
  };
4819
5976
  (0, import_traverse10.default)(ast, {
@@ -4835,13 +5992,13 @@ var WebNavigationAnalyzer = class {
4835
5992
  /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
4836
5993
  componentNameFromElementAttr(opening) {
4837
5994
  for (const attr of opening.attributes) {
4838
- if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
4839
- 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)) {
4840
5997
  const expr = attr.value.expression;
4841
- if (t11.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
5998
+ if (t12.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
4842
5999
  }
4843
- if (attr.name.name === "Component" && t11.isJSXExpressionContainer(attr.value)) {
4844
- 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;
4845
6002
  }
4846
6003
  }
4847
6004
  return null;
@@ -4858,9 +6015,9 @@ var WebNavigationAnalyzer = class {
4858
6015
  (0, import_traverse10.default)(ast, {
4859
6016
  CallExpression: (nodePath) => {
4860
6017
  const callee = nodePath.node.callee;
4861
- if (!t11.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
6018
+ if (!t12.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
4862
6019
  const first = nodePath.node.arguments[0];
4863
- if (!t11.isArrayExpression(first)) return;
6020
+ if (!t12.isArrayExpression(first)) return;
4864
6021
  this.visitRouteObjects(first, "", routes);
4865
6022
  }
4866
6023
  });
@@ -4868,21 +6025,21 @@ var WebNavigationAnalyzer = class {
4868
6025
  }
4869
6026
  visitRouteObjects(arr, parentPath, out) {
4870
6027
  for (const element of arr.elements) {
4871
- if (!t11.isObjectExpression(element)) continue;
6028
+ if (!t12.isObjectExpression(element)) continue;
4872
6029
  let segment;
4873
6030
  let isIndex = false;
4874
6031
  let componentName;
4875
6032
  let children;
4876
6033
  for (const prop of element.properties) {
4877
- if (!t11.isObjectProperty(prop) || !t11.isIdentifier(prop.key)) continue;
6034
+ if (!t12.isObjectProperty(prop) || !t12.isIdentifier(prop.key)) continue;
4878
6035
  const key = prop.key.name;
4879
- if (key === "path" && t11.isStringLiteral(prop.value)) segment = prop.value.value;
4880
- if (key === "index" && t11.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
4881
- 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)) {
4882
6039
  componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
4883
6040
  }
4884
- if (key === "Component" && t11.isIdentifier(prop.value)) componentName = prop.value.name;
4885
- 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;
4886
6043
  }
4887
6044
  const fullPath = this.joinPaths(parentPath, segment, isIndex);
4888
6045
  if ((segment !== void 0 || isIndex) && (componentName || !children)) {
@@ -4960,11 +6117,13 @@ var WebNavigationAnalyzer = class {
4960
6117
  return {
4961
6118
  screens,
4962
6119
  initialScreen: initialRoute?.screenName ?? "",
4963
- navigators: routes.length > 0 ? [{
4964
- name: navigatorName,
4965
- type: WEB_NAVIGATOR_TYPE,
4966
- screens: screenNames
4967
- }] : []
6120
+ navigators: routes.length > 0 ? [
6121
+ {
6122
+ name: navigatorName,
6123
+ type: WEB_NAVIGATOR_TYPE,
6124
+ screens: screenNames
6125
+ }
6126
+ ] : []
4968
6127
  };
4969
6128
  }
4970
6129
  };
@@ -5083,7 +6242,7 @@ var ReactWebPlatformAnalyzer = class {
5083
6242
 
5084
6243
  // src/manifest/loadManifest.ts
5085
6244
  var import_promises3 = require("fs/promises");
5086
- var import_node_path2 = __toESM(require("path"));
6245
+ var import_node_path4 = __toESM(require("path"));
5087
6246
 
5088
6247
  // ../../node_modules/zod/v3/external.js
5089
6248
  var external_exports = {};
@@ -5290,8 +6449,8 @@ var ZodParsedType = util.arrayToEnum([
5290
6449
  "set"
5291
6450
  ]);
5292
6451
  var getParsedType = (data) => {
5293
- const t15 = typeof data;
5294
- switch (t15) {
6452
+ const t16 = typeof data;
6453
+ switch (t16) {
5295
6454
  case "undefined":
5296
6455
  return ZodParsedType.undefined;
5297
6456
  case "string":
@@ -5563,8 +6722,8 @@ function getErrorMap() {
5563
6722
 
5564
6723
  // ../../node_modules/zod/v3/helpers/parseUtil.js
5565
6724
  var makeIssue = (params) => {
5566
- const { data, path: path11, errorMaps, issueData } = params;
5567
- const fullPath = [...path11, ...issueData.path || []];
6725
+ const { data, path: path13, errorMaps, issueData } = params;
6726
+ const fullPath = [...path13, ...issueData.path || []];
5568
6727
  const fullIssue = {
5569
6728
  ...issueData,
5570
6729
  path: fullPath
@@ -5680,11 +6839,11 @@ var errorUtil;
5680
6839
 
5681
6840
  // ../../node_modules/zod/v3/types.js
5682
6841
  var ParseInputLazyPath = class {
5683
- constructor(parent, value, path11, key) {
6842
+ constructor(parent, value, path13, key) {
5684
6843
  this._cachedPath = [];
5685
6844
  this.parent = parent;
5686
6845
  this.data = value;
5687
- this._path = path11;
6846
+ this._path = path13;
5688
6847
  this._key = key;
5689
6848
  }
5690
6849
  get path() {
@@ -9126,7 +10285,18 @@ var coerce = {
9126
10285
  };
9127
10286
  var NEVER = INVALID;
9128
10287
 
9129
- // ../shared/dist/chunk-PKS3VDNB.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
9130
10300
  var locatorSourceSchema = external_exports.enum([
9131
10301
  "appilotsId",
9132
10302
  "testID",
@@ -9305,6 +10475,23 @@ var actionDescriptorSchema = external_exports.object({
9305
10475
  effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
9306
10476
  riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
9307
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(),
9308
10495
  appilotsInferred: appilotsInferredActionSchema.optional()
9309
10496
  }).passthrough();
9310
10497
  var screenPermissionDescriptorSchema = external_exports.object({
@@ -9452,9 +10639,16 @@ var createProjectSchema = external_exports.object({
9452
10639
  // Reference to a tenant-scoped provider (preferred). Optional at
9453
10640
  // creation so a project can be created and configured later.
9454
10641
  providerId: external_exports.string().min(1).optional(),
9455
- systemPrompt: external_exports.string().max(4096).optional()
10642
+ // 16k chars (~4k tokens): um prompt de CS real não cabe em 4096 — o
10643
+ // primeiro cliente de produção chegou com 6.385 e tomou 'Invalid input'
10644
+ // sem saber por quê. O teto continua existindo porque o systemPrompt
10645
+ // entra em TODO hop do relay: é proteção de TPM, não de gosto.
10646
+ systemPrompt: external_exports.string().max(16e3).optional()
10647
+ });
10648
+ var updateProjectSchema = createProjectSchema.partial().extend({
10649
+ providerId: external_exports.string().min(1).nullable().optional(),
10650
+ systemPrompt: external_exports.string().max(16e3).nullable().optional()
9456
10651
  });
9457
- var updateProjectSchema = createProjectSchema.partial();
9458
10652
  var providerKindSchema = external_exports.enum(["openai", "anthropic", "custom"]);
9459
10653
  var createProviderSchema = external_exports.object({
9460
10654
  name: external_exports.string().min(1).max(100),
@@ -9480,7 +10674,7 @@ var uploadMCPDocumentSchema = external_exports.object({
9480
10674
  version: external_exports.string().default("1.0"),
9481
10675
  content: external_exports.record(external_exports.unknown())
9482
10676
  });
9483
- var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
10677
+ var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
9484
10678
  var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
9485
10679
  var createApiKeySchema = external_exports.object({
9486
10680
  name: external_exports.string().min(1).max(100),
@@ -9491,8 +10685,8 @@ var createApiKeySchema = external_exports.object({
9491
10685
  scope: apiKeyScopeSchema.default("sdk"),
9492
10686
  environment: apiKeyEnvironmentSchema.optional(),
9493
10687
  expiresAt: external_exports.string().datetime().optional()
9494
- }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
9495
- 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",
9496
10690
  path: ["projectId"]
9497
10691
  });
9498
10692
  var boundedString = (max) => external_exports.string().max(max);
@@ -9517,6 +10711,15 @@ var snapshotInputSchema = external_exports.object({
9517
10711
  type: boundedString(40).optional(),
9518
10712
  required: external_exports.boolean().optional(),
9519
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(),
9520
10723
  inModal: external_exports.boolean().optional()
9521
10724
  }).passthrough();
9522
10725
  var snapshotButtonSchema = external_exports.object({
@@ -9672,6 +10875,32 @@ var agentSnapshotSchema = external_exports.object({
9672
10875
  * it. Optional: older SDKs never clamp and never send it.
9673
10876
  */
9674
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(),
9675
10904
  /**
9676
10905
  * How many of this screen's controls the client could name, split by
9677
10906
  * `identityProvenance`. Diagnostic only — the relay never grounds an
@@ -9705,7 +10934,19 @@ var agentContextSchema = external_exports.object({
9705
10934
  rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
9706
10935
  currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
9707
10936
  routeNames: external_exports.array(boundedString(200)).max(500).optional(),
9708
- 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()
9709
10950
  }).passthrough().optional(),
9710
10951
  screenMetadata: external_exports.object({
9711
10952
  name: boundedString(200).optional(),
@@ -9811,7 +11052,7 @@ var formFillPayloadSchema = external_exports.object({
9811
11052
  submitAfterFill: external_exports.boolean().optional()
9812
11053
  }).passthrough();
9813
11054
  var uiInteractionPayloadSchema = external_exports.object({
9814
- 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(),
9815
11056
  // `targetId` is the canonical server/LLM field. SDK runtimes still
9816
11057
  // accept `componentId` as a compatibility alias.
9817
11058
  targetId: external_exports.string().min(1),
@@ -9979,8 +11220,15 @@ var continueAgentSchema = external_exports.object({
9979
11220
  // issue #169 — the observation the server grounds targets against, now
9980
11221
  // validated + bounded at the border instead of z.record(z.unknown()).
9981
11222
  context: agentContextSchema.optional(),
9982
- /** Hop counter — server enforces a cap to prevent runaway loops */
9983
- 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()
9984
11232
  });
9985
11233
  var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
9986
11234
  var screenPermissionSchema = external_exports.object({
@@ -10086,15 +11334,18 @@ var themeTokensSchema = external_exports.object({
10086
11334
  */
10087
11335
  mode: external_exports.enum(["auto", "light", "dark"]).optional()
10088
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
+ });
10089
11340
  var projectPersonalizationSchema = external_exports.object({
10090
11341
  /** Tone / persona instructions appended to the system prompt. */
10091
11342
  personaPrompt: external_exports.string().max(2048).nullable(),
10092
11343
  /** Display name in the chat header (e.g. "Aria"). */
10093
11344
  assistantName: external_exports.string().max(60).nullable(),
10094
11345
  /** URL or remote asset id for the avatar shown next to assistant turns. */
10095
- assistantAvatar: external_exports.string().max(500).nullable(),
11346
+ assistantAvatar: imageSourceField.nullable(),
10096
11347
  /** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
10097
- emptyStateIcon: external_exports.string().max(500).nullable(),
11348
+ emptyStateIcon: imageSourceField.nullable(),
10098
11349
  /** First-message text shown in the empty state. */
10099
11350
  welcomeMessage: external_exports.string().max(500).nullable(),
10100
11351
  /** Title rendered at the top of the chat. */
@@ -10108,20 +11359,20 @@ var projectPersonalizationSchema = external_exports.object({
10108
11359
  /** Chat-open FAB background color. Null falls back to theme.colors.primary. */
10109
11360
  triggerButtonColor: hexColorSchema.nullable(),
10110
11361
  /** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
10111
- triggerButtonImageUrl: external_exports.string().max(500).nullable()
11362
+ triggerButtonImageUrl: imageSourceField.nullable()
10112
11363
  });
10113
11364
  var updateProjectPersonalizationSchema = external_exports.object({
10114
11365
  personaPrompt: external_exports.string().max(2048).nullable().optional(),
10115
11366
  assistantName: external_exports.string().max(60).nullable().optional(),
10116
- assistantAvatar: external_exports.string().max(500).nullable().optional(),
10117
- emptyStateIcon: external_exports.string().max(500).nullable().optional(),
11367
+ assistantAvatar: imageSourceField.nullable().optional(),
11368
+ emptyStateIcon: imageSourceField.nullable().optional(),
10118
11369
  welcomeMessage: external_exports.string().max(500).nullable().optional(),
10119
11370
  chatTitle: external_exports.string().max(60).nullable().optional(),
10120
11371
  poweredByVisible: external_exports.boolean().optional(),
10121
11372
  theme: themeTokensSchema.nullable().optional(),
10122
11373
  defaultLocale: localeSchema.nullable().optional(),
10123
11374
  triggerButtonColor: hexColorSchema.nullable().optional(),
10124
- triggerButtonImageUrl: external_exports.string().max(500).nullable().optional()
11375
+ triggerButtonImageUrl: imageSourceField.nullable().optional()
10125
11376
  }).strict();
10126
11377
  var sandboxObservationSchema = external_exports.object({
10127
11378
  route: external_exports.string().max(200).optional(),
@@ -10141,7 +11392,19 @@ var sandboxObservationSchema = external_exports.object({
10141
11392
  type: external_exports.string().max(40).optional(),
10142
11393
  placeholder: external_exports.string().max(200).optional(),
10143
11394
  /** Inline validation state (SDK snapshot field). */
10144
- 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()
10145
11408
  })
10146
11409
  ).max(100).optional(),
10147
11410
  buttons: external_exports.array(
@@ -10599,7 +11862,7 @@ var manifestSchema = external_exports.object({
10599
11862
  navigation: navigationGraphSchema.partial().optional()
10600
11863
  }).passthrough();
10601
11864
  async function loadManifest(rootDir, manifestPath) {
10602
- 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);
10603
11866
  let raw;
10604
11867
  try {
10605
11868
  raw = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
@@ -10748,7 +12011,7 @@ var MCPGenerator = class _MCPGenerator {
10748
12011
  const agentReadyScreens = mergedScreens.map(
10749
12012
  (screen) => enrichScreenForAgent({
10750
12013
  ...screen,
10751
- 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
10752
12015
  })
10753
12016
  );
10754
12017
  const projectInfo = await this.getProjectInfo();
@@ -10772,11 +12035,11 @@ var MCPGenerator = class _MCPGenerator {
10772
12035
  }
10773
12036
  };
10774
12037
  const serialized = JSON.stringify(document, null, 2);
10775
- const checksum = this.calculateChecksum(serialized);
10776
- 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}`);
10777
12040
  await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
10778
12041
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10779
- const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
12042
+ const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
10780
12043
  await (0, import_promises4.writeFile)(checksumFilePath, checksum, "utf-8");
10781
12044
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
10782
12045
  console.log("[MCPGenerator] Generation complete!");
@@ -10784,7 +12047,8 @@ var MCPGenerator = class _MCPGenerator {
10784
12047
  document,
10785
12048
  filePath,
10786
12049
  format: this.options.format,
10787
- checksum
12050
+ checksum,
12051
+ ...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
10788
12052
  };
10789
12053
  }
10790
12054
  /**
@@ -10796,7 +12060,7 @@ var MCPGenerator = class _MCPGenerator {
10796
12060
  * after calling `generate()`.
10797
12061
  */
10798
12062
  static async readPreviousChecksum(outputDir) {
10799
- const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
12063
+ const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
10800
12064
  try {
10801
12065
  const content = await (0, import_promises4.readFile)(checksumFilePath, "utf-8");
10802
12066
  return content.trim() || null;
@@ -10815,7 +12079,7 @@ var MCPGenerator = class _MCPGenerator {
10815
12079
  */
10816
12080
  async getProjectInfo() {
10817
12081
  try {
10818
- 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");
10819
12083
  const packageJsonContent = await (0, import_promises4.readFile)(packageJsonPath, "utf-8");
10820
12084
  const packageJson = JSON.parse(packageJsonContent);
10821
12085
  return {
@@ -10890,8 +12154,8 @@ function reportMetadataLint(document, strict) {
10890
12154
  }
10891
12155
 
10892
12156
  // src/cli/utils/remote-drift.ts
10893
- var import_node_fs = require("fs");
10894
- var import_node_path4 = require("path");
12157
+ var import_node_fs3 = require("fs");
12158
+ var import_node_path6 = require("path");
10895
12159
  var STUB_RATIO = 0.5;
10896
12160
  function compareWithRemote(local, remote) {
10897
12161
  if (remote.screensCount === local.screens) return null;
@@ -10904,7 +12168,7 @@ function compareWithRemote(local, remote) {
10904
12168
  }
10905
12169
  function readLocalDocumentSummary(outputDir) {
10906
12170
  try {
10907
- 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");
10908
12172
  const parsed = JSON.parse(raw);
10909
12173
  if (typeof parsed?.metadata?.totalScreens !== "number") return null;
10910
12174
  return { screens: parsed.metadata.totalScreens };
@@ -10989,14 +12253,18 @@ function assertDocumentHasScreens(document, options = {}) {
10989
12253
  }
10990
12254
  error("No screens found \u2014 this application map would give the agent nothing to navigate.");
10991
12255
  info("");
10992
- info("The analyzer looks for React Navigation: a <NavigationContainer> and");
10993
- 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.");
10994
12259
  info("");
10995
12260
  info("The two usual causes:");
10996
- info(" \u2022 The app uses Expo Router (or another file-based router). It is the");
10997
- info(" default for `npx create-expo-app` and the analyzer cannot read it.");
10998
- info(" \u2022 The screens live outside the analyzed paths \u2014 check `include`,");
10999
- 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.");
11000
12268
  info("");
11001
12269
  info("The escape hatch, which works for any router: declare your screens in");
11002
12270
  info("an `appilots.manifest.json` at the project root. Its screens are merged");
@@ -11043,7 +12311,7 @@ function syncCommand() {
11043
12311
  const outputDir = config.outputDir || ".appilots";
11044
12312
  let previousChecksum = "";
11045
12313
  try {
11046
- const checksumPath = (0, import_node_path5.join)(outputDir, ".appilots-checksum");
12314
+ const checksumPath = (0, import_node_path7.join)(outputDir, ".appilots-checksum");
11047
12315
  previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
11048
12316
  } catch {
11049
12317
  }
@@ -11170,7 +12438,7 @@ function syncCommand() {
11170
12438
 
11171
12439
  // src/cli/commands/watch.ts
11172
12440
  var import_commander3 = require("commander");
11173
- var import_node_fs2 = require("fs");
12441
+ var import_node_fs4 = require("fs");
11174
12442
  function watchCommand() {
11175
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) => {
11176
12444
  try {
@@ -11217,7 +12485,7 @@ function watchCommand() {
11217
12485
  dim("Press Ctrl+C to stop\n");
11218
12486
  let debounceTimer = null;
11219
12487
  const DEBOUNCE_MS = 500;
11220
- 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) => {
11221
12489
  if (!filename) return;
11222
12490
  if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
11223
12491
  return;
@@ -11269,6 +12537,95 @@ function watchCommand() {
11269
12537
 
11270
12538
  // src/cli/commands/generate.ts
11271
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
11272
12629
  function generateCommand() {
11273
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(
11274
12631
  "--strict-metadata",
@@ -11285,7 +12642,7 @@ function generateCommand() {
11285
12642
  spinner.start();
11286
12643
  let loaded = null;
11287
12644
  try {
11288
- loaded = loadConfig(warn);
12645
+ loaded = loadConfig(warn, { requireApiKey: false });
11289
12646
  } catch {
11290
12647
  loaded = null;
11291
12648
  }
@@ -11293,11 +12650,21 @@ function generateCommand() {
11293
12650
  rootDir: process.cwd(),
11294
12651
  outputDir: options.output || ".appilots",
11295
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,
11296
12659
  platform: loaded?.platform,
11297
12660
  manifestPath: loaded?.manifestPath
11298
12661
  });
11299
12662
  const output = await generator.generate();
11300
12663
  spinner.stop();
12664
+ reportReach({
12665
+ document: output.document,
12666
+ ...output.diagnostics?.navigation ? { navigation: output.diagnostics.navigation } : {}
12667
+ });
11301
12668
  assertDocumentHasScreens(output.document, {
11302
12669
  rootDir: process.cwd(),
11303
12670
  allowEmpty: Boolean(options.allowEmpty)
@@ -11317,19 +12684,18 @@ function generateCommand() {
11317
12684
 
11318
12685
  // src/cli/commands/annotate.ts
11319
12686
  var import_promises6 = require("fs/promises");
11320
- var import_node_path7 = __toESM(require("path"));
12687
+ var import_node_path9 = __toESM(require("path"));
11321
12688
  var import_commander5 = require("commander");
11322
- var import_fast_glob6 = __toESM(require("fast-glob"));
11323
12689
 
11324
12690
  // src/annotate/sites.ts
11325
12691
  var import_traverse12 = __toESM(require("@babel/traverse"));
11326
- var t13 = __toESM(require("@babel/types"));
12692
+ var t14 = __toESM(require("@babel/types"));
11327
12693
 
11328
12694
  // src/annotate/forwarding.ts
11329
- var import_node_path6 = __toESM(require("path"));
11330
- var import_node_fs3 = require("fs");
12695
+ var import_node_path8 = __toESM(require("path"));
12696
+ var import_node_fs5 = require("fs");
11331
12697
  var import_traverse11 = __toESM(require("@babel/traverse"));
11332
- var t12 = __toESM(require("@babel/types"));
12698
+ var t13 = __toESM(require("@babel/types"));
11333
12699
  var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
11334
12700
  var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
11335
12701
  "View",
@@ -11361,10 +12727,10 @@ function collectImports(ast) {
11361
12727
  ImportDeclaration(nodePath) {
11362
12728
  const source = nodePath.node.source.value;
11363
12729
  for (const spec of nodePath.node.specifiers) {
11364
- if (t12.isImportSpecifier(spec)) {
11365
- 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;
11366
12732
  out.set(spec.local.name, { source, local: spec.local.name, imported });
11367
- } else if (t12.isImportDefaultSpecifier(spec)) {
12733
+ } else if (t13.isImportDefaultSpecifier(spec)) {
11368
12734
  out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
11369
12735
  }
11370
12736
  }
@@ -11372,17 +12738,17 @@ function collectImports(ast) {
11372
12738
  });
11373
12739
  return out;
11374
12740
  }
11375
- var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
12741
+ var EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
11376
12742
  function resolveRelativeImport(fromFile, source) {
11377
12743
  if (!source.startsWith(".")) return void 0;
11378
- const base = import_node_path6.default.resolve(import_node_path6.default.dirname(fromFile), source);
11379
- 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) {
11380
12746
  const candidate = `${base}${ext}`;
11381
- if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
12747
+ if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
11382
12748
  }
11383
- for (const ext of EXTENSIONS) {
11384
- const candidate = import_node_path6.default.join(base, `index${ext}`);
11385
- 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;
11386
12752
  }
11387
12753
  return void 0;
11388
12754
  }
@@ -11397,7 +12763,7 @@ function inspectModule(file2, exportName, depth = 0) {
11397
12763
  if (depth > 2) return none;
11398
12764
  let source;
11399
12765
  try {
11400
- source = (0, import_node_fs3.readFileSync)(file2, "utf-8");
12766
+ source = (0, import_node_fs5.readFileSync)(file2, "utf-8");
11401
12767
  } catch {
11402
12768
  return none;
11403
12769
  }
@@ -11418,7 +12784,7 @@ function inspectModule(file2, exportName, depth = 0) {
11418
12784
  const from = nodePath.node.source?.value;
11419
12785
  if (!from) return;
11420
12786
  const named = nodePath.node.specifiers.some(
11421
- (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
11422
12788
  );
11423
12789
  if (named) barrelTarget = from;
11424
12790
  },
@@ -11427,11 +12793,11 @@ function inspectModule(file2, exportName, depth = 0) {
11427
12793
  },
11428
12794
  // `interface CardProps { testID?: string }` / `type Props = { testID… }`
11429
12795
  TSPropertySignature(nodePath) {
11430
- 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;
11431
12797
  },
11432
12798
  // `function Card({ testID }) {}` — the destructured parameter.
11433
12799
  ObjectProperty(nodePath) {
11434
- 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;
11435
12801
  },
11436
12802
  // `<View {...props} />` — anything spread onto JSX carries it along.
11437
12803
  JSXSpreadAttribute() {
@@ -11442,17 +12808,17 @@ function inspectModule(file2, exportName, depth = 0) {
11442
12808
  // that is one hardcoded name shared by every instance, which is the
11443
12809
  // duplicate-id problem rather than a solution to it.
11444
12810
  JSXAttribute(nodePath) {
11445
- const name = t12.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
12811
+ const name = t13.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
11446
12812
  if (name !== "testID" && name !== "accessibilityLabel") return;
11447
12813
  const value = nodePath.node.value;
11448
- if (!t12.isJSXExpressionContainer(value)) return;
11449
- if (t12.isStringLiteral(value.expression)) return;
12814
+ if (!t13.isJSXExpressionContainer(value)) return;
12815
+ if (t13.isStringLiteral(value.expression)) return;
11450
12816
  derives = true;
11451
12817
  },
11452
12818
  // `useAppilotsTarget(`${groupId}-${option.value}`, …)`
11453
12819
  CallExpression(nodePath) {
11454
12820
  const callee = nodePath.node.callee;
11455
- if (t12.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
12821
+ if (t13.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
11456
12822
  }
11457
12823
  });
11458
12824
  if (forwards || derives) return { forwards, derives };
@@ -11537,18 +12903,18 @@ function hasLabelText(element) {
11537
12903
  function namedHandler(element, prop) {
11538
12904
  if (!prop) return void 0;
11539
12905
  const attr = findJsxAttribute(element, prop);
11540
- if (!attr || !t13.isJSXExpressionContainer(attr.value)) return void 0;
11541
- 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;
11542
12908
  }
11543
12909
  var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
11544
12910
  function isPerItemRender(nodePath) {
11545
12911
  let current = nodePath.parentPath;
11546
12912
  while (current) {
11547
12913
  const node = current.node;
11548
- 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")) {
11549
12915
  return true;
11550
12916
  }
11551
- 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") {
11552
12918
  return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
11553
12919
  }
11554
12920
  current = current.parentPath;
@@ -11578,7 +12944,7 @@ function findAnnotationSites(source, file2) {
11578
12944
  traverse12(ast, {
11579
12945
  JSXOpeningElement(nodePath) {
11580
12946
  const element = nodePath.node;
11581
- if (!t13.isJSXIdentifier(element.name)) return;
12947
+ if (!t14.isJSXIdentifier(element.name)) return;
11582
12948
  const component = element.name.name;
11583
12949
  const role = classifyJsxComponent(component, element);
11584
12950
  if (!ANNOTATABLE_ROLES.has(role)) return;
@@ -11660,10 +13026,10 @@ function applyToFile(source, sites) {
11660
13026
  }
11661
13027
 
11662
13028
  // src/annotate/forward.ts
11663
- var import_node_fs4 = require("fs");
13029
+ var import_node_fs6 = require("fs");
11664
13030
  var import_magic_string2 = __toESM(require("magic-string"));
11665
13031
  var import_traverse13 = __toESM(require("@babel/traverse"));
11666
- var t14 = __toESM(require("@babel/types"));
13032
+ var t15 = __toESM(require("@babel/types"));
11667
13033
  var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
11668
13034
  function findComponent(ast, name) {
11669
13035
  let found;
@@ -11674,9 +13040,9 @@ function findComponent(ast, name) {
11674
13040
  }
11675
13041
  },
11676
13042
  VariableDeclarator(nodePath) {
11677
- 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;
11678
13044
  const init = nodePath.node.init;
11679
- if (t14.isArrowFunctionExpression(init) || t14.isFunctionExpression(init)) {
13045
+ if (t15.isArrowFunctionExpression(init) || t15.isFunctionExpression(init)) {
11680
13046
  found ??= { params: init.params, body: init.body };
11681
13047
  }
11682
13048
  }
@@ -11688,26 +13054,26 @@ function findReturnedRoots(body) {
11688
13054
  let unsupported = false;
11689
13055
  const record = (node) => {
11690
13056
  if (!node) return;
11691
- if (t14.isParenthesizedExpression(node)) return record(node.expression);
11692
- if (t14.isJSXElement(node)) {
13057
+ if (t15.isParenthesizedExpression(node)) return record(node.expression);
13058
+ if (t15.isJSXElement(node)) {
11693
13059
  roots.push(node.openingElement);
11694
13060
  return;
11695
13061
  }
11696
- if (t14.isConditionalExpression(node)) {
13062
+ if (t15.isConditionalExpression(node)) {
11697
13063
  record(node.consequent);
11698
13064
  record(node.alternate);
11699
13065
  return;
11700
13066
  }
11701
- if (t14.isJSXFragment(node)) {
13067
+ if (t15.isJSXFragment(node)) {
11702
13068
  unsupported = true;
11703
13069
  return;
11704
13070
  }
11705
- if (t14.isNullLiteral(node)) return;
13071
+ if (t15.isNullLiteral(node)) return;
11706
13072
  unsupported = true;
11707
13073
  };
11708
- if (t14.isBlockStatement(body)) {
13074
+ if (t15.isBlockStatement(body)) {
11709
13075
  traverse13(
11710
- t14.file(t14.program([t14.expressionStatement(t14.functionExpression(null, [], body))])),
13076
+ t15.file(t15.program([t15.expressionStatement(t15.functionExpression(null, [], body))])),
11711
13077
  {
11712
13078
  ReturnStatement(nodePath) {
11713
13079
  record(nodePath.node.argument);
@@ -11721,7 +13087,7 @@ function findReturnedRoots(body) {
11721
13087
  return roots;
11722
13088
  }
11723
13089
  function isCarrierRoot(element, imports) {
11724
- if (!t14.isJSXIdentifier(element.name)) return false;
13090
+ if (!t15.isJSXIdentifier(element.name)) return false;
11725
13091
  const name = element.name.name;
11726
13092
  const origin = imports.get(name);
11727
13093
  if (origin?.source !== "react-native") return false;
@@ -11729,7 +13095,7 @@ function isCarrierRoot(element, imports) {
11729
13095
  }
11730
13096
  function hasProp(element, name) {
11731
13097
  return element.attributes.some(
11732
- (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
11733
13099
  );
11734
13100
  }
11735
13101
  function indentOf(source, offset) {
@@ -11760,7 +13126,7 @@ function followBarrel(file2, ast, component) {
11760
13126
  const from = nodePath.node.source?.value;
11761
13127
  if (!from) return;
11762
13128
  const named = nodePath.node.specifiers.some(
11763
- (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
11764
13130
  );
11765
13131
  if (named) target ??= from;
11766
13132
  }
@@ -11771,7 +13137,7 @@ function planForward(file2, component, depth = 0) {
11771
13137
  const base = { file: file2, component, edits: [], roots: 0 };
11772
13138
  let source;
11773
13139
  try {
11774
- source = (0, import_node_fs4.readFileSync)(file2, "utf-8");
13140
+ source = (0, import_node_fs6.readFileSync)(file2, "utf-8");
11775
13141
  } catch {
11776
13142
  return { ...base, refusal: `cannot read ${file2}` };
11777
13143
  }
@@ -11788,7 +13154,7 @@ function planForward(file2, component, depth = 0) {
11788
13154
  return { ...base, refusal: `no function named ${component} in this file` };
11789
13155
  }
11790
13156
  const param = found.params[0];
11791
- if (!t14.isObjectPattern(param)) {
13157
+ if (!t15.isObjectPattern(param)) {
11792
13158
  return {
11793
13159
  ...base,
11794
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`
@@ -11804,7 +13170,7 @@ function planForward(file2, component, depth = 0) {
11804
13170
  const imports = collectImports(ast);
11805
13171
  const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
11806
13172
  if (nonCarrier) {
11807
- const name = t14.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
13173
+ const name = t15.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
11808
13174
  return {
11809
13175
  ...base,
11810
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`
@@ -11812,21 +13178,21 @@ function planForward(file2, component, depth = 0) {
11812
13178
  }
11813
13179
  const edits = [];
11814
13180
  const alreadyDestructured = param.properties.some(
11815
- (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"
11816
13182
  );
11817
13183
  if (!alreadyDestructured) {
11818
13184
  const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
11819
13185
  edits.push({ ...insert, what: "parameter" });
11820
13186
  }
11821
13187
  const annotation = param.typeAnnotation;
11822
- if (t14.isTSTypeAnnotation(annotation)) {
13188
+ if (t15.isTSTypeAnnotation(annotation)) {
11823
13189
  const declared = annotation.typeAnnotation;
11824
13190
  let body;
11825
13191
  let closing = 0;
11826
- if (t14.isTSTypeLiteral(declared)) {
13192
+ if (t15.isTSTypeLiteral(declared)) {
11827
13193
  body = declared.members;
11828
13194
  closing = declared.end ?? 0;
11829
- } else if (t14.isTSTypeReference(declared) && t14.isIdentifier(declared.typeName)) {
13195
+ } else if (t15.isTSTypeReference(declared) && t15.isIdentifier(declared.typeName)) {
11830
13196
  const typeName = declared.typeName.name;
11831
13197
  traverse13(ast, {
11832
13198
  TSInterfaceDeclaration(nodePath) {
@@ -11835,7 +13201,7 @@ function planForward(file2, component, depth = 0) {
11835
13201
  closing = nodePath.node.body.end ?? 0;
11836
13202
  },
11837
13203
  TSTypeAliasDeclaration(nodePath) {
11838
- if (nodePath.node.id.name !== typeName || !t14.isTSTypeLiteral(nodePath.node.typeAnnotation))
13204
+ if (nodePath.node.id.name !== typeName || !t15.isTSTypeLiteral(nodePath.node.typeAnnotation))
11839
13205
  return;
11840
13206
  body = nodePath.node.typeAnnotation.members;
11841
13207
  closing = nodePath.node.typeAnnotation.end ?? 0;
@@ -11850,7 +13216,7 @@ function planForward(file2, component, depth = 0) {
11850
13216
  }
11851
13217
  if (body) {
11852
13218
  const declaresTestId = body.some(
11853
- (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"
11854
13220
  );
11855
13221
  if (!declaresTestId) {
11856
13222
  const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
@@ -11907,7 +13273,7 @@ function annotateCommand() {
11907
13273
  const spinner = createSpinner("Scanning for unaddressable controls...");
11908
13274
  spinner.start();
11909
13275
  resetForwardingCache();
11910
- 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 });
11911
13277
  const scan = async () => {
11912
13278
  const out = [];
11913
13279
  for (const file2 of files) {
@@ -11956,9 +13322,9 @@ function annotateCommand() {
11956
13322
  written: options.write === true,
11957
13323
  annotated: written.length,
11958
13324
  blocked: blocked.length,
11959
- 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)),
11960
13326
  sites: [...written, ...blocked].map((site) => ({
11961
- file: import_node_path7.default.relative(rootDir, site.file),
13327
+ file: import_node_path9.default.relative(rootDir, site.file),
11962
13328
  line: site.line,
11963
13329
  component: site.component,
11964
13330
  role: site.role,
@@ -12001,9 +13367,12 @@ async function planForwards(edits, rootDir) {
12001
13367
  void rootDir;
12002
13368
  return plans;
12003
13369
  }
13370
+ function escapeRegExp(value) {
13371
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13372
+ }
12004
13373
  function findImportSource(source, name) {
12005
13374
  const pattern = new RegExp(
12006
- String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
13375
+ String.raw`import\s*\{[^}]*\b${escapeRegExp(name)}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
12007
13376
  );
12008
13377
  return pattern.exec(source)?.[1];
12009
13378
  }
@@ -12012,7 +13381,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12012
13381
  if (written.length > 0) {
12013
13382
  heading(didWrite ? "Added" : "Would add");
12014
13383
  for (const edit of touched) {
12015
- info(import_node_path7.default.relative(rootDir, edit.file));
13384
+ info(import_node_path9.default.relative(rootDir, edit.file));
12016
13385
  for (const site of edit.written) {
12017
13386
  dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
12018
13387
  }
@@ -12024,7 +13393,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12024
13393
  if (teachable.size > 0) {
12025
13394
  heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
12026
13395
  for (const plan of teachable.values()) {
12027
- info(`<${plan.component}> ${import_node_path7.default.relative(rootDir, plan.file)}`);
13396
+ info(`<${plan.component}> ${import_node_path9.default.relative(rootDir, plan.file)}`);
12028
13397
  for (const edit of plan.edits) {
12029
13398
  dim(` ${edit.what}: ${edit.text.trim()}`);
12030
13399
  }
@@ -12332,10 +13701,10 @@ function matchActions(expected, actual, reply = "") {
12332
13701
  var import_fs9 = require("fs");
12333
13702
  var import_path8 = require("path");
12334
13703
  var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
12335
- function loadBaseline(path11) {
12336
- 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: {} };
12337
13706
  try {
12338
- const raw = JSON.parse((0, import_fs9.readFileSync)(path11, "utf-8"));
13707
+ const raw = JSON.parse((0, import_fs9.readFileSync)(path13, "utf-8"));
12339
13708
  return {
12340
13709
  generatedAt: raw.generatedAt ?? null,
12341
13710
  gitSha: raw.gitSha ?? null,
@@ -12345,9 +13714,9 @@ function loadBaseline(path11) {
12345
13714
  return { ...EMPTY_BASELINE, perScenario: {} };
12346
13715
  }
12347
13716
  }
12348
- function saveBaseline(path11, baseline) {
12349
- (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path11), { recursive: true });
12350
- (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)}
12351
13720
  `, "utf-8");
12352
13721
  }
12353
13722
  function buildBaseline(outcomes, meta) {
@@ -12375,11 +13744,11 @@ function diffAgainstBaseline(outcomes, baseline, maxTokenRegression) {
12375
13744
  continue;
12376
13745
  }
12377
13746
  if (outcome.status === "passed" && prior.status === "passed" && prior.totalTokens > 0 && outcome.totalTokens > prior.totalTokens * (1 + maxTokenRegression)) {
12378
- const pct = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
13747
+ const pct2 = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
12379
13748
  regressions.push({
12380
13749
  name: outcome.name,
12381
13750
  kind: "token-regression",
12382
- 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)}%)`
12383
13752
  });
12384
13753
  }
12385
13754
  }