@appilots/cli 0.11.0 → 0.13.0

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
@@ -28,10 +28,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  ));
29
29
 
30
30
  // src/cli/index.ts
31
- var import_commander8 = require("commander");
31
+ var import_commander9 = require("commander");
32
32
 
33
33
  // src/version.ts
34
- var CLI_VERSION = "0.11.0";
34
+ var CLI_VERSION = "0.13.0";
35
35
 
36
36
  // src/config/index.ts
37
37
  var import_fs = require("fs");
@@ -52,7 +52,8 @@ var KNOWN_CONFIG_KEYS = [
52
52
  "navigationExclude",
53
53
  "platform",
54
54
  "manifestPath",
55
- "eval"
55
+ "eval",
56
+ "knowledge"
56
57
  ];
57
58
  var KEY_ALIASES = {
58
59
  apiUrl: "serverUrl",
@@ -107,12 +108,12 @@ function getEnvOverrides(env = process.env) {
107
108
  return trimmed ? trimmed : void 0;
108
109
  };
109
110
  return {
110
- apiKey: clean(env.APPILOTS_API_KEY),
111
+ apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
111
112
  projectId: clean(env.APPILOTS_PROJECT_ID),
112
113
  serverUrl: clean(env.APPILOTS_SERVER_URL)
113
114
  };
114
115
  }
115
- function loadConfig(onWarn) {
116
+ function loadConfig(onWarn, options = {}) {
116
117
  const configPath = getConfigPath();
117
118
  const env = getEnvOverrides();
118
119
  let fileConfig = {};
@@ -151,6 +152,10 @@ function loadConfig(onWarn) {
151
152
  };
152
153
  const validation = validateConfig(merged);
153
154
  validation.warnings.unshift(...legacyWarnings);
155
+ if (options.requireApiKey === false) {
156
+ validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
157
+ validation.valid = validation.errors.length === 0;
158
+ }
154
159
  if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
155
160
  for (const key of Object.keys(fileConfig)) {
156
161
  if (KNOWN_CONFIG_KEYS.includes(key)) continue;
@@ -196,7 +201,8 @@ function saveConfig(dir, config) {
196
201
  navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
197
202
  platform: config.platform || existingConfig?.platform,
198
203
  manifestPath: config.manifestPath || existingConfig?.manifestPath,
199
- eval: config.eval || existingConfig?.eval
204
+ eval: config.eval || existingConfig?.eval,
205
+ knowledge: config.knowledge || existingConfig?.knowledge
200
206
  };
201
207
  try {
202
208
  (0, import_fs.writeFileSync)(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
@@ -289,6 +295,19 @@ function validateConfig(config) {
289
295
  if (config.manifestPath !== void 0 && typeof config.manifestPath !== "string") {
290
296
  errors.push("manifestPath must be a string");
291
297
  }
298
+ if (config.knowledge !== void 0) {
299
+ if (typeof config.knowledge !== "object" || config.knowledge === null || Array.isArray(config.knowledge)) {
300
+ errors.push("knowledge must be an object");
301
+ } else {
302
+ const kc = config.knowledge;
303
+ for (const field of ["sources", "exclude"]) {
304
+ const value = kc[field];
305
+ if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
306
+ errors.push(`knowledge.${field} must be an array of strings`);
307
+ }
308
+ }
309
+ }
310
+ }
292
311
  if (config.eval !== void 0) {
293
312
  if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
294
313
  errors.push("eval must be an object");
@@ -630,6 +649,48 @@ var AppilotsAPIClient = class {
630
649
  };
631
650
  }
632
651
  }
652
+ /**
653
+ * Syncs knowledge documents with the Appilots backend.
654
+ *
655
+ * @param documents Array of documents to sync (filename, base64 content, mimeType, checksum)
656
+ * @returns KnowledgeSyncResult with counts of uploaded, skipped, and errored docs
657
+ */
658
+ async knowledgeSync(documents) {
659
+ try {
660
+ const response = await this.request(`${this.baseUrl}/cli/knowledge/sync`, {
661
+ method: "POST",
662
+ headers: {
663
+ "Content-Type": "application/json",
664
+ Authorization: `Bearer ${this.apiKey}`
665
+ },
666
+ body: JSON.stringify({ documents })
667
+ });
668
+ if (!response.ok) {
669
+ const errorData = await response.json().catch(() => ({}));
670
+ return {
671
+ success: false,
672
+ uploaded: 0,
673
+ skipped: 0,
674
+ errors: 0,
675
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
676
+ };
677
+ }
678
+ const json = await response.json();
679
+ const inner = json.data ?? json;
680
+ return {
681
+ success: true,
682
+ ...inner
683
+ };
684
+ } catch (error2) {
685
+ return {
686
+ success: false,
687
+ uploaded: 0,
688
+ skipped: 0,
689
+ errors: 0,
690
+ error: error2 instanceof Error ? error2.message : "Failed to sync knowledge with Appilots API"
691
+ };
692
+ }
693
+ }
633
694
  /**
634
695
  * Checks if the Appilots API server is healthy
635
696
  *
@@ -763,12 +824,18 @@ function initCommand() {
763
824
  // src/cli/commands/sync.ts
764
825
  var import_commander2 = require("commander");
765
826
  var import_promises5 = require("fs/promises");
766
- var import_node_path5 = require("path");
827
+ var import_node_path7 = require("path");
767
828
 
768
829
  // src/generators/MCPGenerator.ts
769
830
  var import_promises4 = require("fs/promises");
770
- var import_node_crypto = require("crypto");
771
- var import_node_path3 = __toESM(require("path"));
831
+ var import_node_crypto2 = require("crypto");
832
+ var import_node_path5 = __toESM(require("path"));
833
+
834
+ // src/generators/checksum.ts
835
+ function serializeForChecksum(document) {
836
+ const { generatedAt: _generatedAt, ...content } = document;
837
+ return JSON.stringify(content, null, 2);
838
+ }
772
839
 
773
840
  // src/pipeline/enrichment.ts
774
841
  function enrichScreenForAgent(screen) {
@@ -846,7 +913,10 @@ function mergeTargets(targets) {
846
913
  for (const target of targets) {
847
914
  if (!target.id) continue;
848
915
  const existing = byId.get(target.id);
849
- byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
916
+ byId.set(
917
+ target.id,
918
+ existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
919
+ );
850
920
  }
851
921
  return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
852
922
  }
@@ -891,7 +961,10 @@ function synthesizeFlows(screen, targets) {
891
961
  intent: "destructive_action",
892
962
  steps: [
893
963
  { type: "press", target: action.id, label: action.label },
894
- { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
964
+ {
965
+ type: "confirm",
966
+ description: "Wait for native or custom confirmation before continuing"
967
+ },
895
968
  { type: "wait", description: describeWait(action) }
896
969
  ],
897
970
  waitPolicy: waitPolicyForAction(action),
@@ -906,10 +979,19 @@ function synthesizeFlows(screen, targets) {
906
979
  title: `Act on an item in ${collection.id}`,
907
980
  intent: "list_action",
908
981
  steps: [
909
- { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
910
- { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
982
+ {
983
+ type: "choose-list-item",
984
+ target: collection.id,
985
+ description: "Resolve the user reference to a visible or searchable row"
986
+ },
987
+ {
988
+ type: "press",
989
+ description: collection.rowAction?.description ?? "Open the row action"
990
+ }
911
991
  ],
912
- waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
992
+ waitPolicy: {
993
+ expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
994
+ }
913
995
  });
914
996
  }
915
997
  }
@@ -917,25 +999,32 @@ function synthesizeFlows(screen, targets) {
917
999
  }
918
1000
  function waitPolicyForAction(action) {
919
1001
  const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
920
- const signals = [action.successSignal, action.failureSignal].filter(Boolean);
1002
+ const signals = [action.successSignal, action.failureSignal].filter(
1003
+ Boolean
1004
+ );
1005
+ const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
921
1006
  return {
922
1007
  ...expectedOutcome ? { expectedOutcome } : {},
923
1008
  ...signals && signals.length > 0 ? { signals } : {},
924
- ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
1009
+ ...maxMs !== void 0 ? { maxMs } : {}
925
1010
  };
926
1011
  }
927
1012
  function describeWait(action) {
928
1013
  if (action.successSignal?.description) return action.successSignal.description;
929
- if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
1014
+ if (action.successSignal?.type === "goBack")
1015
+ return "Wait for the app to return to the previous screen";
930
1016
  if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
931
- if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
1017
+ if (action.appilotsInferred?.expectedOutcome)
1018
+ return `Wait for ${action.appilotsInferred.expectedOutcome}`;
932
1019
  return "Wait for the UI to settle";
933
1020
  }
934
1021
  function synthesizeAgentHints(screen, targets, flows) {
935
1022
  const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
936
1023
  const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
937
1024
  const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
938
- const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
1025
+ const firstAsyncAction = screen.actions.find(
1026
+ (action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
1027
+ );
939
1028
  const hints = {
940
1029
  primaryGoal: synthesizePrimaryGoal(screen),
941
1030
  commonTasks,
@@ -962,7 +1051,9 @@ function synthesizePrimaryGoal(screen) {
962
1051
  }
963
1052
  const fieldCount = uniqueFields.size;
964
1053
  if (fieldCount > 0) {
965
- const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
1054
+ const requiredCount = Array.from(uniqueFields.values()).filter(
1055
+ (field) => field.required
1056
+ ).length;
966
1057
  const fieldLabel = fieldCount === 1 ? "field" : "fields";
967
1058
  goals.push(
968
1059
  requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
@@ -976,7 +1067,9 @@ function synthesizePrimaryGoal(screen) {
976
1067
  if (submitCount > 0) {
977
1068
  goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
978
1069
  }
979
- const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
1070
+ const asyncCount = screen.actions.filter(
1071
+ (action) => action.appilotsInferred?.isAsyncTrigger
1072
+ ).length;
980
1073
  if (asyncCount > 0) {
981
1074
  goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
982
1075
  }
@@ -986,15 +1079,370 @@ function synthesizePrimaryGoal(screen) {
986
1079
  }
987
1080
 
988
1081
  // src/analyzers/ReactNativePlatformAnalyzer.ts
989
- var import_node_path = __toESM(require("path"));
990
- var import_fast_glob3 = __toESM(require("fast-glob"));
1082
+ var import_node_path3 = __toESM(require("path"));
1083
+
1084
+ // src/utils/glob.ts
1085
+ var import_fast_glob = __toESM(require("fast-glob"));
1086
+ function byCodeUnit(a, b) {
1087
+ return a < b ? -1 : a > b ? 1 : 0;
1088
+ }
1089
+ async function globSorted(patterns, options) {
1090
+ const files = await (0, import_fast_glob.default)(patterns, options);
1091
+ return files.sort(byCodeUnit);
1092
+ }
991
1093
 
992
1094
  // src/analyzers/ScreenAnalyzer.ts
993
1095
  var import_promises = __toESM(require("fs/promises"));
1096
+
1097
+ // src/extractors/control-evidence.ts
1098
+ var import_node_crypto = require("crypto");
1099
+ var import_traverse = __toESM(require("@babel/traverse"));
1100
+ var t2 = __toESM(require("@babel/types"));
1101
+
1102
+ // src/extractors/control-evidence-bindings.ts
1103
+ var t = __toESM(require("@babel/types"));
1104
+ var MAX_HOPS = 8;
1105
+ function unwrap(path13) {
1106
+ while (path13.isTSAsExpression() || path13.isTSTypeAssertion() || path13.isTSNonNullExpression() || path13.isTSSatisfiesExpression() || path13.isParenthesizedExpression())
1107
+ path13 = path13.get("expression");
1108
+ return path13;
1109
+ }
1110
+ function constantValue(path13, depth = 0) {
1111
+ if (depth > MAX_HOPS) return void 0;
1112
+ path13 = unwrap(path13);
1113
+ if (!path13.isIdentifier()) return path13;
1114
+ const binding = path13.scope.getBinding(path13.node.name);
1115
+ if (!binding?.constant || !binding.path.isVariableDeclarator()) return path13;
1116
+ const init = binding.path.get("init");
1117
+ const resolved = init.node ? constantValue(init, depth + 1) : void 0;
1118
+ if (resolved?.isObjectExpression() && binding.referencePaths.some(
1119
+ (reference) => !reference.parentPath?.isJSXSpreadAttribute() && !reference.parentPath?.isSpreadElement()
1120
+ ))
1121
+ return void 0;
1122
+ return resolved;
1123
+ }
1124
+ function propValue(path13, name) {
1125
+ const readObject = (path14) => {
1126
+ const resolved = constantValue(path14);
1127
+ if (!resolved?.isObjectExpression()) return { blocked: true };
1128
+ for (const property of [...resolved.get("properties")].reverse()) {
1129
+ if (property.isSpreadElement()) {
1130
+ const found = readObjectBounded(property.get("argument"));
1131
+ if (found.value || found.blocked) return found;
1132
+ } else if (property.isObjectProperty() || property.isObjectMethod()) {
1133
+ if (property.node.computed) return { blocked: true };
1134
+ const key = property.node.key;
1135
+ if ((t.isIdentifier(key) ? key.name : t.isStringLiteral(key) ? key.value : "") === name)
1136
+ return property.isObjectProperty() ? { value: property.get("value") } : { value: property };
1137
+ }
1138
+ }
1139
+ return {};
1140
+ };
1141
+ let objectBudget = MAX_HOPS;
1142
+ const readObjectBounded = (path14) => objectBudget-- > 0 ? readObject(path14) : { blocked: true };
1143
+ for (const attr of [...path13.get("attributes")].reverse()) {
1144
+ if (attr.isJSXAttribute() && t.isJSXIdentifier(attr.node.name, { name })) {
1145
+ const value = attr.get("value");
1146
+ return value.isJSXExpressionContainer() ? unwrap(value.get("expression")) : value.node ? value : void 0;
1147
+ }
1148
+ if (attr.isJSXSpreadAttribute()) {
1149
+ const found = readObjectBounded(attr.get("argument"));
1150
+ if (found.value || found.blocked) return found.value;
1151
+ }
1152
+ }
1153
+ return void 0;
1154
+ }
1155
+ function importIdentity(path13, depth = 0) {
1156
+ if (depth > MAX_HOPS) return void 0;
1157
+ path13 = unwrap(path13);
1158
+ if (path13.isIdentifier() || path13.isJSXIdentifier()) {
1159
+ const binding = path13.scope.getBinding(path13.node.name);
1160
+ if (!binding?.constant) return void 0;
1161
+ const declaration = binding.path;
1162
+ if (declaration.parentPath?.isImportDeclaration()) {
1163
+ const module2 = declaration.parentPath.node.source.value;
1164
+ if (declaration.isImportSpecifier()) {
1165
+ const imported = declaration.node.imported;
1166
+ return { module: module2, imported: t.isIdentifier(imported) ? imported.name : imported.value };
1167
+ }
1168
+ if (declaration.isImportDefaultSpecifier()) return { module: module2, imported: "default" };
1169
+ if (declaration.isImportNamespaceSpecifier()) return { module: module2, imported: "*" };
1170
+ }
1171
+ if (declaration.isVariableDeclarator() && declaration.get("init").node)
1172
+ return importIdentity(declaration.get("init"), depth + 1);
1173
+ }
1174
+ if (path13.isJSXMemberExpression() || path13.isMemberExpression() && !path13.node.computed) {
1175
+ const origin = importIdentity(path13.get("object"), depth + 1);
1176
+ const key = path13.node.property;
1177
+ if (origin && (t.isIdentifier(key) || t.isJSXIdentifier(key)) && (origin.imported === "*" || origin.module === "react" && origin.imported === "default"))
1178
+ return { module: origin.module, imported: key.name };
1179
+ }
1180
+ return void 0;
1181
+ }
1182
+ function handlerFunction(path13, depth = 0) {
1183
+ if (depth > MAX_HOPS) return void 0;
1184
+ path13 = unwrap(path13);
1185
+ if (path13.isFunction()) return path13;
1186
+ if (path13.isIdentifier()) {
1187
+ const binding = path13.scope.getBinding(path13.node.name);
1188
+ if (!binding?.constant) return void 0;
1189
+ if (binding.path.isFunctionDeclaration()) return binding.path;
1190
+ if (binding.path.isVariableDeclarator() && binding.path.get("init").node)
1191
+ return handlerFunction(binding.path.get("init"), depth + 1);
1192
+ }
1193
+ if (path13.isCallExpression()) {
1194
+ const origin = importIdentity(path13.get("callee"));
1195
+ if (origin?.module === "react" && origin.imported === "useCallback") {
1196
+ const callback = path13.get("arguments")[0];
1197
+ if (callback) return handlerFunction(callback, depth + 1);
1198
+ }
1199
+ }
1200
+ if (path13.isMemberExpression() && !path13.node.computed && t.isThisExpression(path13.node.object)) {
1201
+ const key = path13.node.property;
1202
+ if (!t.isIdentifier(key)) return void 0;
1203
+ const owner = path13.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
1204
+ if (!owner || !(owner.isClassDeclaration() || owner.isClassExpression())) return void 0;
1205
+ for (const member of owner.get("body").get("body")) {
1206
+ if (!(member.isClassMethod() || member.isClassProperty()) || member.node.computed || member.node.static)
1207
+ continue;
1208
+ if (!t.isIdentifier(member.node.key, { name: key.name })) continue;
1209
+ if (member.isClassMethod() && member.node.kind === "method") return member;
1210
+ if (member.isClassProperty() && member.get("value").node)
1211
+ return handlerFunction(member.get("value"), depth + 1);
1212
+ }
1213
+ }
1214
+ return void 0;
1215
+ }
1216
+
1217
+ // src/extractors/control-evidence.ts
1218
+ function symbol(node) {
1219
+ if (t2.isTSAsExpression(node) || t2.isTSTypeAssertion(node) || t2.isTSNonNullExpression(node) || t2.isTSSatisfiesExpression(node))
1220
+ return symbol(node.expression);
1221
+ if (t2.isIdentifier(node) || t2.isJSXIdentifier(node)) return node.name;
1222
+ if (t2.isThisExpression(node)) return "this";
1223
+ if (t2.isMemberExpression(node) && !node.computed || t2.isJSXMemberExpression(node)) {
1224
+ const object = symbol(node.object), property = symbol(node.property);
1225
+ return object && property ? `${object}.${property}` : void 0;
1226
+ }
1227
+ if (t2.isUnaryExpression(node) && node.operator === "!") {
1228
+ const value = symbol(node.argument);
1229
+ return value ? `!${value}` : void 0;
1230
+ }
1231
+ return void 0;
1232
+ }
1233
+ function attribute(node, name) {
1234
+ return node.attributes.find(
1235
+ (a) => t2.isJSXAttribute(a) && t2.isJSXIdentifier(a.name, { name })
1236
+ );
1237
+ }
1238
+ var ICON_LIBRARIES = [
1239
+ "lucide-react-native",
1240
+ "lucide-react",
1241
+ "@tamagui/lucide-icons",
1242
+ "@expo/vector-icons",
1243
+ "@react-native-vector-icons/",
1244
+ "react-native-vector-icons/"
1245
+ ];
1246
+ function iconLibrary(module2) {
1247
+ return ICON_LIBRARIES.some(
1248
+ (name) => name.endsWith("/") ? module2.startsWith(name) : module2 === name || module2.startsWith(name + "/")
1249
+ );
1250
+ }
1251
+ function add(values, value, max = 12) {
1252
+ if (value && value.length <= 240 && values.length < max && !values.includes(value))
1253
+ values.push(value);
1254
+ }
1255
+ function iconName(path13, explicitIcon = false) {
1256
+ const origin = importIdentity(path13);
1257
+ if (origin && iconLibrary(origin.module) && origin.imported !== "*")
1258
+ return origin.imported === "default" ? origin.module : `${origin.module}:${origin.imported}`;
1259
+ const name = symbol(path13.node);
1260
+ return name && (explicitIcon || /icon/i.test(name)) ? name : void 0;
1261
+ }
1262
+ function iconsInElement(path13, explicitIcon = false) {
1263
+ const name = iconName(path13.get("name"), explicitIcon);
1264
+ if (!name) return void 0;
1265
+ const glyph = propValue(path13, "name");
1266
+ const value = glyph && constantValue(glyph);
1267
+ return value?.isStringLiteral() ? `${name}:${value.node.value}` : name;
1268
+ }
1269
+ function hasInteraction(path13) {
1270
+ return ["onPress", "onLongPress", "onClick"].some(
1271
+ (name) => attribute(path13.node, name) || propValue(path13, name)
1272
+ );
1273
+ }
1274
+ function collectPresentationIcons(path13, icons) {
1275
+ const namedIconProps = [
1276
+ "icon",
1277
+ "prefix",
1278
+ "suffix",
1279
+ "left",
1280
+ "right",
1281
+ "leadingIcon",
1282
+ "trailingIcon",
1283
+ "startIcon",
1284
+ "endIcon",
1285
+ "renderIcon"
1286
+ ];
1287
+ const props = new Set(namedIconProps);
1288
+ for (const attr of path13.node.attributes)
1289
+ if (t2.isJSXAttribute(attr) && t2.isJSXIdentifier(attr.name) && !/^on[A-Z]/.test(attr.name.name))
1290
+ props.add(attr.name.name);
1291
+ for (const prop of props) {
1292
+ const icon = propValue(path13, prop);
1293
+ if (icon) {
1294
+ const value = constantValue(icon);
1295
+ if (value?.isStringLiteral()) {
1296
+ if (prop.toLowerCase().includes("icon")) add(icons, value.node.value, 8);
1297
+ } else {
1298
+ const explicitIcon = prop.toLowerCase() === "icon";
1299
+ if (icon.isJSXElement()) {
1300
+ if (hasInteraction(icon.get("openingElement"))) continue;
1301
+ add(icons, iconsInElement(icon.get("openingElement"), explicitIcon), 8);
1302
+ }
1303
+ icon.traverse({
1304
+ JSXAttribute(attr) {
1305
+ if (t2.isJSXIdentifier(attr.node.name) && /^on[A-Z]/.test(attr.node.name.name))
1306
+ attr.skip();
1307
+ },
1308
+ JSXElement(child) {
1309
+ const opening = child.get("openingElement");
1310
+ if (hasInteraction(opening)) {
1311
+ child.skip();
1312
+ return;
1313
+ }
1314
+ add(icons, iconsInElement(opening, explicitIcon), 8);
1315
+ }
1316
+ });
1317
+ if (!icons.length && (namedIconProps.includes(prop) || explicitIcon))
1318
+ add(icons, iconName(icon, explicitIcon), 8);
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1323
+ function collectIcons(path13) {
1324
+ const icons = [];
1325
+ collectPresentationIcons(path13, icons);
1326
+ const jsx = path13.parentPath;
1327
+ if (jsx.isJSXElement())
1328
+ jsx.traverse({
1329
+ // JSX mentioned inside an event callback is not a rendered child icon.
1330
+ JSXAttribute(attr) {
1331
+ attr.skip();
1332
+ },
1333
+ JSXElement(child) {
1334
+ const opening = child.get("openingElement");
1335
+ if (hasInteraction(opening)) {
1336
+ child.skip();
1337
+ return;
1338
+ }
1339
+ add(icons, iconsInElement(opening), 8);
1340
+ collectPresentationIcons(opening, icons);
1341
+ }
1342
+ });
1343
+ return icons;
1344
+ }
1345
+ function conditionsAt(path13) {
1346
+ const conditions = [];
1347
+ let child = path13;
1348
+ for (let parent = child.parentPath; parent && !parent.isFunction(); child = parent, parent = parent.parentPath) {
1349
+ let test;
1350
+ let negated = false;
1351
+ if (parent.isLogicalExpression() && parent.node.right === child.node) {
1352
+ if (parent.node.operator === "&&") test = parent.node.left;
1353
+ if (parent.node.operator === "||") {
1354
+ test = parent.node.left;
1355
+ negated = true;
1356
+ }
1357
+ } else if (parent.isConditionalExpression() && parent.node.test !== child.node) {
1358
+ test = parent.node.test;
1359
+ negated = parent.node.alternate === child.node;
1360
+ } else if (parent.isIfStatement() && parent.node.test !== child.node) {
1361
+ test = parent.node.test;
1362
+ negated = parent.node.alternate === child.node;
1363
+ }
1364
+ const name = symbol(test);
1365
+ if (name) add(conditions, negated ? name.startsWith("!") ? name.slice(1) : `!${name}` : name);
1366
+ }
1367
+ return conditions;
1368
+ }
1369
+ function collectHandlerEvidence(expression, evidence) {
1370
+ const seen = /* @__PURE__ */ new Set();
1371
+ let budget = 100;
1372
+ const visit = (path13, depth) => {
1373
+ if (depth > 4 || budget-- <= 0) return;
1374
+ const fn = handlerFunction(path13);
1375
+ if (!fn || seen.has(fn.node)) return;
1376
+ seen.add(fn.node);
1377
+ fn.traverse({
1378
+ // Ignore uncalled helper definitions; inline callbacks remain source evidence.
1379
+ Function(nested) {
1380
+ if (!nested.parentPath.isCallExpression() && !nested.parentPath.isObjectProperty())
1381
+ nested.skip();
1382
+ },
1383
+ CallExpression(call) {
1384
+ if (budget-- <= 0) {
1385
+ call.skip();
1386
+ return;
1387
+ }
1388
+ add(evidence.calls, symbol(call.node.callee));
1389
+ for (const arg of call.node.arguments) add(evidence.argumentBindings, symbol(arg));
1390
+ visit(call.get("callee"), depth + 1);
1391
+ if (!t2.isMemberExpression(call.node.callee) || call.node.callee.computed || !t2.isIdentifier(call.node.callee.property, { name: "alert" }))
1392
+ return;
1393
+ const callee = call.get("callee");
1394
+ if (!callee.isMemberExpression()) return;
1395
+ const origin = importIdentity(callee.get("object"));
1396
+ if (origin?.module !== "react-native" || origin.imported !== "Alert") return;
1397
+ const options = call.node.arguments[2];
1398
+ const destructiveOption = t2.isArrayExpression(options) && options.elements.some(
1399
+ (option) => t2.isObjectExpression(option) && option.properties.some(
1400
+ (p) => t2.isObjectProperty(p) && !p.computed && symbol(p.key) === "style" && t2.isStringLiteral(p.value, { value: "destructive" })
1401
+ )
1402
+ );
1403
+ if (destructiveOption)
1404
+ evidence.nativeConfirmation = {
1405
+ title: symbol(call.node.arguments[0]),
1406
+ destructiveOption
1407
+ };
1408
+ }
1409
+ });
1410
+ };
1411
+ visit(unwrap(expression), 0);
1412
+ }
1413
+ function extractControlEvidence(ast, source, file2) {
1414
+ const candidates = [];
1415
+ const sourceHash = (0, import_node_crypto.createHash)("sha256").update(source).digest("hex");
1416
+ (0, import_traverse.default)(ast, {
1417
+ JSXOpeningElement(path13) {
1418
+ const node = path13.node;
1419
+ if (attribute(node, "__appilotsControl")) return;
1420
+ const value = propValue(path13, "onPress");
1421
+ if (!value && !attribute(node, "onPress") || value?.isNullLiteral() || value?.isJSXEmptyExpression())
1422
+ return;
1423
+ const icons = collectIcons(path13);
1424
+ if (!icons.length || node.end == null) return;
1425
+ const evidence = {
1426
+ version: 1,
1427
+ siteId: (0, import_node_crypto.createHash)("sha256").update(`${file2}:${sourceHash}:${node.start}`).digest("hex").slice(0, 20),
1428
+ component: symbol(node.name) ?? "unknown",
1429
+ icons,
1430
+ ...value && symbol(value.node) ? { handler: symbol(value.node) } : {},
1431
+ calls: [],
1432
+ argumentBindings: [],
1433
+ conditions: conditionsAt(path13)
1434
+ };
1435
+ if (value) collectHandlerEvidence(value, evidence);
1436
+ candidates.push({ evidence, sourceHash, offset: node.end - (node.selfClosing ? 2 : 1) });
1437
+ }
1438
+ });
1439
+ return candidates;
1440
+ }
1441
+
1442
+ // src/analyzers/ScreenAnalyzer.ts
994
1443
  var import_path3 = __toESM(require("path"));
995
- var import_traverse4 = __toESM(require("@babel/traverse"));
1444
+ var import_traverse5 = __toESM(require("@babel/traverse"));
996
1445
  var BabelTypes = __toESM(require("@babel/types"));
997
- var import_fast_glob = __toESM(require("fast-glob"));
998
1446
 
999
1447
  // src/ast/parse.ts
1000
1448
  var import_parser = require("@babel/parser");
@@ -1018,25 +1466,25 @@ function parseSource(source, parserPlugins = []) {
1018
1466
  }
1019
1467
 
1020
1468
  // src/ast/jsx/attributes.ts
1021
- var t = __toESM(require("@babel/types"));
1469
+ var t3 = __toESM(require("@babel/types"));
1022
1470
  function findJsxAttribute(element, name) {
1023
1471
  return element.attributes.find(
1024
- (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === name
1472
+ (attr) => t3.isJSXAttribute(attr) && t3.isJSXIdentifier(attr.name) && attr.name.name === name
1025
1473
  );
1026
1474
  }
1027
1475
  function getStringAttr(element, name) {
1028
1476
  const attr = findJsxAttribute(element, name);
1029
1477
  if (!attr || !attr.value) return void 0;
1030
- if (t.isStringLiteral(attr.value)) return attr.value.value;
1031
- if (t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)) {
1478
+ if (t3.isStringLiteral(attr.value)) return attr.value.value;
1479
+ if (t3.isJSXExpressionContainer(attr.value) && t3.isStringLiteral(attr.value.expression)) {
1032
1480
  return attr.value.expression.value;
1033
1481
  }
1034
1482
  return void 0;
1035
1483
  }
1036
1484
  function getExpressionIdentifierAttr(element, name) {
1037
1485
  const attr = findJsxAttribute(element, name);
1038
- if (!attr?.value || !t.isJSXExpressionContainer(attr.value)) return void 0;
1039
- return t.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
1486
+ if (!attr?.value || !t3.isJSXExpressionContainer(attr.value)) return void 0;
1487
+ return t3.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
1040
1488
  }
1041
1489
  function hasJsxAttribute(element, name) {
1042
1490
  return Boolean(findJsxAttribute(element, name));
@@ -1115,18 +1563,18 @@ function classifyJsxComponent(name, element) {
1115
1563
  }
1116
1564
 
1117
1565
  // src/ast/functions/collect.ts
1118
- var import_traverse = __toESM(require("@babel/traverse"));
1119
- var t2 = __toESM(require("@babel/types"));
1566
+ var import_traverse2 = __toESM(require("@babel/traverse"));
1567
+ var t4 = __toESM(require("@babel/types"));
1120
1568
  function collectFunctions(ast) {
1121
1569
  const handlers = /* @__PURE__ */ new Map();
1122
- (0, import_traverse.default)(ast, {
1570
+ (0, import_traverse2.default)(ast, {
1123
1571
  FunctionDeclaration: (nodePath) => {
1124
1572
  if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
1125
1573
  },
1126
1574
  VariableDeclarator: (nodePath) => {
1127
- if (!t2.isIdentifier(nodePath.node.id)) return;
1575
+ if (!t4.isIdentifier(nodePath.node.id)) return;
1128
1576
  const init = nodePath.node.init;
1129
- if (t2.isArrowFunctionExpression(init) || t2.isFunctionExpression(init)) {
1577
+ if (t4.isArrowFunctionExpression(init) || t4.isFunctionExpression(init)) {
1130
1578
  handlers.set(nodePath.node.id.name, init);
1131
1579
  }
1132
1580
  }
@@ -1135,8 +1583,8 @@ function collectFunctions(ast) {
1135
1583
  }
1136
1584
 
1137
1585
  // src/ast/functions/analyze-async.ts
1138
- var import_traverse2 = __toESM(require("@babel/traverse"));
1139
- var t3 = __toESM(require("@babel/types"));
1586
+ var import_traverse3 = __toESM(require("@babel/traverse"));
1587
+ var t5 = __toESM(require("@babel/types"));
1140
1588
  var DESTRUCTIVE_VERB = /(delete|destroy|remove|discard|wipe|erase|drop|terminate|revoke|deactivate|disable)/i;
1141
1589
  function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new Set()) {
1142
1590
  if (seen.has(name)) {
@@ -1158,41 +1606,41 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
1158
1606
  let destructive = DESTRUCTIVE_VERB.test(name);
1159
1607
  const loadingStateBindings = /* @__PURE__ */ new Set();
1160
1608
  const inspectCall = (node) => {
1161
- if (t3.isMemberExpression(node.callee) && t3.isIdentifier(node.callee.property)) {
1609
+ if (t5.isMemberExpression(node.callee) && t5.isIdentifier(node.callee.property)) {
1162
1610
  const method = node.callee.property.name;
1163
1611
  if (method === "then" || method === "catch" || method === "finally") hasThen = true;
1164
- if (t3.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && ["navigate", "push", "replace"].includes(method)) {
1612
+ if (t5.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && ["navigate", "push", "replace"].includes(method)) {
1165
1613
  const firstArg = node.arguments[0];
1166
- if (t3.isStringLiteral(firstArg)) {
1614
+ if (t5.isStringLiteral(firstArg)) {
1167
1615
  targetScreen = firstArg.value;
1168
1616
  successSignal = { type: "navigation", target: firstArg.value };
1169
1617
  }
1170
1618
  }
1171
- if (t3.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && method === "goBack") {
1619
+ if (t5.isIdentifier(node.callee.object) && node.callee.object.name === "navigation" && method === "goBack") {
1172
1620
  successSignal = { type: "goBack", description: "Action returns to the previous screen" };
1173
1621
  }
1174
- if (t3.isIdentifier(node.callee.object) && node.callee.object.name === "Alert" && method === "alert") {
1622
+ if (t5.isIdentifier(node.callee.object) && node.callee.object.name === "Alert" && method === "alert") {
1175
1623
  nativeConfirmationExpected = true;
1176
1624
  }
1177
- if (t3.isIdentifier(node.callee.object) && /(toast|notification|snackbar)/i.test(node.callee.object.name)) {
1625
+ if (t5.isIdentifier(node.callee.object) && /(toast|notification|snackbar)/i.test(node.callee.object.name)) {
1178
1626
  const firstArg = node.arguments[0];
1179
1627
  const secondArg = node.arguments[1];
1180
- const text = t3.isStringLiteral(secondArg) ? secondArg.value : t3.isStringLiteral(firstArg) ? firstArg.value : void 0;
1628
+ const text = t5.isStringLiteral(secondArg) ? secondArg.value : t5.isStringLiteral(firstArg) ? firstArg.value : void 0;
1181
1629
  const signal = { type: "toast", description: text };
1182
- if (/(error|danger|fail)/i.test(method) || t3.isStringLiteral(firstArg) && /error/i.test(firstArg.value)) {
1630
+ if (/(error|danger|fail)/i.test(method) || t5.isStringLiteral(firstArg) && /error/i.test(firstArg.value)) {
1183
1631
  failureSignal = signal;
1184
1632
  } else {
1185
1633
  successSignal = successSignal ?? signal;
1186
1634
  }
1187
1635
  }
1188
1636
  }
1189
- if (t3.isIdentifier(node.callee)) {
1637
+ if (t5.isIdentifier(node.callee)) {
1190
1638
  const calleeName = node.callee.name;
1191
1639
  if (/^set[A-Z]/.test(calleeName)) {
1192
1640
  hasStateSetter = true;
1193
1641
  const stateName = setterToStateName(calleeName);
1194
1642
  const firstArg = node.arguments[0];
1195
- if (stateName && t3.isBooleanLiteral(firstArg)) {
1643
+ if (stateName && t5.isBooleanLiteral(firstArg)) {
1196
1644
  if (/loading|submitting|saving|fetching|refreshing/i.test(stateName)) {
1197
1645
  loadingStateBindings.add(stateName);
1198
1646
  }
@@ -1219,14 +1667,14 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
1219
1667
  }
1220
1668
  };
1221
1669
  const inspectNode = (node) => {
1222
- if (t3.isAwaitExpression(node)) hasAwait = true;
1223
- if (t3.isCallExpression(node)) inspectCall(node);
1224
- if (t3.isMemberExpression(node) && t3.isIdentifier(node.property) && DESTRUCTIVE_VERB.test(node.property.name)) {
1670
+ if (t5.isAwaitExpression(node)) hasAwait = true;
1671
+ if (t5.isCallExpression(node)) inspectCall(node);
1672
+ if (t5.isMemberExpression(node) && t5.isIdentifier(node.property) && DESTRUCTIVE_VERB.test(node.property.name)) {
1225
1673
  destructive = true;
1226
1674
  }
1227
1675
  };
1228
1676
  if (fn.body) {
1229
- (0, import_traverse2.default)(fn.body, {
1677
+ (0, import_traverse3.default)(fn.body, {
1230
1678
  noScope: true,
1231
1679
  enter: (nodePath) => inspectNode(nodePath.node)
1232
1680
  });
@@ -1254,21 +1702,21 @@ function setterToStateName(setterName) {
1254
1702
  }
1255
1703
 
1256
1704
  // src/ast/navigation/calls.ts
1257
- var import_traverse3 = __toESM(require("@babel/traverse"));
1258
- var t4 = __toESM(require("@babel/types"));
1705
+ var import_traverse4 = __toESM(require("@babel/traverse"));
1706
+ var t6 = __toESM(require("@babel/types"));
1259
1707
  function extractNavigationCalls(ast) {
1260
1708
  const calls = [];
1261
- (0, import_traverse3.default)(ast, {
1262
- noScope: !t4.isFile(ast),
1709
+ (0, import_traverse4.default)(ast, {
1710
+ noScope: !t6.isFile(ast),
1263
1711
  CallExpression: (nodePath) => {
1264
1712
  const node = nodePath.node;
1265
- if (!t4.isMemberExpression(node.callee) || !t4.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !t4.isIdentifier(node.callee.property)) {
1713
+ if (!t6.isMemberExpression(node.callee) || !t6.isIdentifier(node.callee.object) || node.callee.object.name !== "navigation" || !t6.isIdentifier(node.callee.property)) {
1266
1714
  return;
1267
1715
  }
1268
1716
  const method = node.callee.property.name;
1269
1717
  if (!["navigate", "push", "replace", "goBack"].includes(method)) return;
1270
1718
  const firstArg = node.arguments[0];
1271
- const targetScreen = t4.isStringLiteral(firstArg) ? firstArg.value : void 0;
1719
+ const targetScreen = t6.isStringLiteral(firstArg) ? firstArg.value : void 0;
1272
1720
  calls.push({
1273
1721
  method,
1274
1722
  ...targetScreen ? { targetScreen } : {},
@@ -1280,16 +1728,16 @@ function extractNavigationCalls(ast) {
1280
1728
  }
1281
1729
  function extractNavigationParams(arg) {
1282
1730
  const params = {};
1283
- if (!arg || !t4.isObjectExpression(arg)) return params;
1731
+ if (!arg || !t6.isObjectExpression(arg)) return params;
1284
1732
  for (const prop of arg.properties) {
1285
- if (!t4.isObjectProperty(prop)) continue;
1286
- const key = t4.isIdentifier(prop.key) ? prop.key.name : t4.isStringLiteral(prop.key) ? prop.key.value : void 0;
1733
+ if (!t6.isObjectProperty(prop)) continue;
1734
+ const key = t6.isIdentifier(prop.key) ? prop.key.name : t6.isStringLiteral(prop.key) ? prop.key.value : void 0;
1287
1735
  if (!key) continue;
1288
- if (t4.isMemberExpression(prop.value) && t4.isIdentifier(prop.value.object) && t4.isIdentifier(prop.value.property)) {
1736
+ if (t6.isMemberExpression(prop.value) && t6.isIdentifier(prop.value.object) && t6.isIdentifier(prop.value.property)) {
1289
1737
  params[key] = `${prop.value.object.name}.${prop.value.property.name}`;
1290
- } else if (t4.isIdentifier(prop.value)) {
1738
+ } else if (t6.isIdentifier(prop.value)) {
1291
1739
  params[key] = prop.value.name;
1292
- } else if (t4.isStringLiteral(prop.value)) {
1740
+ } else if (t6.isStringLiteral(prop.value)) {
1293
1741
  params[key] = prop.value.value;
1294
1742
  }
1295
1743
  }
@@ -1397,14 +1845,26 @@ var ScreenAnalyzer = class {
1397
1845
  strictScreens;
1398
1846
  /** Glob patterns that identify screen files in strict mode */
1399
1847
  screenPatterns;
1848
+ /**
1849
+ * Arquivos que uma ROTA monta (`component={…}` resolvido pelo
1850
+ * `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
1851
+ * convenção de nome, porque um componente que uma rota monta É uma tela por
1852
+ * definição — não é heurística, é o que o app declarou.
1853
+ *
1854
+ * É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
1855
+ * em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
1856
+ */
1857
+ routeTargetFiles;
1400
1858
  /** §D: Count of screens filtered out in strict mode (available after analyze()) */
1401
1859
  screensFilteredOut = 0;
1860
+ controlEvidenceFiles = {};
1402
1861
  constructor(config, options) {
1403
1862
  this.config = config;
1863
+ this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
1404
1864
  this.strictScreens = options?.strictScreens ?? false;
1405
1865
  this.screenPatterns = options?.screenPatterns ?? [
1406
- "**/*Screen.{ts,tsx}",
1407
- "**/screens/**/*.{ts,tsx}"
1866
+ "**/*Screen.{ts,tsx,js,jsx}",
1867
+ "**/screens/**/*.{ts,tsx,js,jsx}"
1408
1868
  ];
1409
1869
  }
1410
1870
  /** Analyze all screens in the project */
@@ -1416,7 +1876,7 @@ var ScreenAnalyzer = class {
1416
1876
  console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
1417
1877
  }
1418
1878
  }
1419
- const files = await (0, import_fast_glob.default)(include, {
1879
+ const files = await globSorted(include, {
1420
1880
  cwd: this.config.rootDir,
1421
1881
  ignore: exclude
1422
1882
  });
@@ -1425,7 +1885,7 @@ var ScreenAnalyzer = class {
1425
1885
  }
1426
1886
  let screenPatternFiles = null;
1427
1887
  if (this.strictScreens) {
1428
- const matched = await (0, import_fast_glob.default)(this.screenPatterns, {
1888
+ const matched = await globSorted(this.screenPatterns, {
1429
1889
  cwd: this.config.rootDir,
1430
1890
  ignore: exclude
1431
1891
  });
@@ -1441,7 +1901,8 @@ var ScreenAnalyzer = class {
1441
1901
  if (this.strictScreens) {
1442
1902
  const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
1443
1903
  const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
1444
- if (!hasRegisterScreen && !matchesPattern) {
1904
+ const isRouteTarget = this.routeTargetFiles.has(filePath);
1905
+ if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
1445
1906
  this.screensFilteredOut++;
1446
1907
  if (this.verbose) {
1447
1908
  console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file2}`);
@@ -1497,6 +1958,11 @@ var ScreenAnalyzer = class {
1497
1958
  title: registerScreenMeta?.title,
1498
1959
  description: registerScreenMeta?.description,
1499
1960
  components,
1961
+ controlCandidates: extractControlEvidence(
1962
+ ast,
1963
+ source,
1964
+ import_path3.default.relative(this.config.rootDir, filePath)
1965
+ ),
1500
1966
  forms,
1501
1967
  actions,
1502
1968
  navigationTargets,
@@ -1507,6 +1973,8 @@ var ScreenAnalyzer = class {
1507
1973
  ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
1508
1974
  } : {}
1509
1975
  };
1976
+ if (descriptor.controlCandidates?.length)
1977
+ this.controlEvidenceFiles[import_path3.default.relative(this.config.rootDir, filePath).split(import_path3.default.sep).join("/")] = descriptor.controlCandidates;
1510
1978
  descriptor.__hasRegisterScreen = hasRegisterScreenCall;
1511
1979
  return descriptor;
1512
1980
  }
@@ -1516,7 +1984,7 @@ var ScreenAnalyzer = class {
1516
1984
  */
1517
1985
  detectRegisterScreenCall(ast) {
1518
1986
  let found = false;
1519
- (0, import_traverse4.default)(ast, {
1987
+ (0, import_traverse5.default)(ast, {
1520
1988
  CallExpression: (nodePath) => {
1521
1989
  if (found) return;
1522
1990
  const callee = nodePath.node.callee;
@@ -1533,7 +2001,7 @@ var ScreenAnalyzer = class {
1533
2001
  */
1534
2002
  extractRegisterScreenMetadata(ast) {
1535
2003
  let metadata = null;
1536
- (0, import_traverse4.default)(ast, {
2004
+ (0, import_traverse5.default)(ast, {
1537
2005
  CallExpression: (nodePath) => {
1538
2006
  const callee = nodePath.node.callee;
1539
2007
  if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
@@ -1649,6 +2117,8 @@ var ScreenAnalyzer = class {
1649
2117
  action.riskLevel = value.value;
1650
2118
  } else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
1651
2119
  action.nativeConfirmationExpected = value.value;
2120
+ } else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
2121
+ action.asyncBudgetMs = value.value;
1652
2122
  } else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
1653
2123
  action.appilotsInferred = this.parseAppilotsInferredObject(value);
1654
2124
  }
@@ -1828,7 +2298,7 @@ var ScreenAnalyzer = class {
1828
2298
  */
1829
2299
  extractDefaultComponentName(ast) {
1830
2300
  let componentName = "";
1831
- (0, import_traverse4.default)(ast, {
2301
+ (0, import_traverse5.default)(ast, {
1832
2302
  ExportDefaultDeclaration: (nodePath) => {
1833
2303
  const declaration = nodePath.node.declaration;
1834
2304
  if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
@@ -1853,7 +2323,7 @@ var ScreenAnalyzer = class {
1853
2323
  */
1854
2324
  extractNavigationTargets(ast) {
1855
2325
  const targets = /* @__PURE__ */ new Set();
1856
- (0, import_traverse4.default)(ast, {
2326
+ (0, import_traverse5.default)(ast, {
1857
2327
  CallExpression: (nodePath) => {
1858
2328
  const callee = nodePath.node.callee;
1859
2329
  if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "navigate") {
@@ -1872,7 +2342,7 @@ var ScreenAnalyzer = class {
1872
2342
  extractForms(ast) {
1873
2343
  const forms = [];
1874
2344
  const fields = /* @__PURE__ */ new Map();
1875
- (0, import_traverse4.default)(ast, {
2345
+ (0, import_traverse5.default)(ast, {
1876
2346
  JSXOpeningElement: (nodePath) => {
1877
2347
  const element = nodePath.node;
1878
2348
  if (BabelTypes.isJSXIdentifier(element.name)) {
@@ -1993,7 +2463,7 @@ var ScreenAnalyzer = class {
1993
2463
  extractComponents(ast) {
1994
2464
  const components = [];
1995
2465
  const seen = /* @__PURE__ */ new Set();
1996
- (0, import_traverse4.default)(ast, {
2466
+ (0, import_traverse5.default)(ast, {
1997
2467
  JSXOpeningElement: (nodePath) => {
1998
2468
  const element = nodePath.node;
1999
2469
  if (BabelTypes.isJSXIdentifier(element.name)) {
@@ -2057,7 +2527,7 @@ var ScreenAnalyzer = class {
2057
2527
  const actionLabels = new Map(
2058
2528
  actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
2059
2529
  );
2060
- (0, import_traverse4.default)(ast, {
2530
+ (0, import_traverse5.default)(ast, {
2061
2531
  JSXOpeningElement: (nodePath) => {
2062
2532
  const element = nodePath.node;
2063
2533
  if (BabelTypes.isJSXIdentifier(element.name)) {
@@ -2126,7 +2596,7 @@ var ScreenAnalyzer = class {
2126
2596
  }
2127
2597
  collectButtonHandlersByLabel(ast) {
2128
2598
  const out = /* @__PURE__ */ new Map();
2129
- (0, import_traverse4.default)(ast, {
2599
+ (0, import_traverse5.default)(ast, {
2130
2600
  JSXOpeningElement: (nodePath) => {
2131
2601
  const element = nodePath.node;
2132
2602
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
@@ -2208,7 +2678,7 @@ var ScreenAnalyzer = class {
2208
2678
  }
2209
2679
  };
2210
2680
  if (fn.body) {
2211
- (0, import_traverse4.default)(
2681
+ (0, import_traverse5.default)(
2212
2682
  fn.body,
2213
2683
  {
2214
2684
  noScope: true,
@@ -2405,7 +2875,7 @@ var ScreenAnalyzer = class {
2405
2875
  extractCollections(ast) {
2406
2876
  const renderItemFns = this.collectRenderItemFunctions(ast);
2407
2877
  const collections = [];
2408
- (0, import_traverse4.default)(ast, {
2878
+ (0, import_traverse5.default)(ast, {
2409
2879
  JSXOpeningElement: (nodePath) => {
2410
2880
  const element = nodePath.node;
2411
2881
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
@@ -2442,7 +2912,7 @@ var ScreenAnalyzer = class {
2442
2912
  }
2443
2913
  collectRenderItemFunctions(ast) {
2444
2914
  const out = /* @__PURE__ */ new Map();
2445
- (0, import_traverse4.default)(ast, {
2915
+ (0, import_traverse5.default)(ast, {
2446
2916
  VariableDeclarator: (nodePath) => {
2447
2917
  if (!BabelTypes.isIdentifier(nodePath.node.id)) return;
2448
2918
  const init = nodePath.node.init;
@@ -2485,7 +2955,7 @@ var ScreenAnalyzer = class {
2485
2955
  }
2486
2956
  extractRowAction(fn) {
2487
2957
  let action;
2488
- (0, import_traverse4.default)(
2958
+ (0, import_traverse5.default)(
2489
2959
  fn.body,
2490
2960
  {
2491
2961
  noScope: true,
@@ -2538,7 +3008,7 @@ var ScreenAnalyzer = class {
2538
3008
  } else if (BabelTypes.isIdentifier(firstParam)) {
2539
3009
  itemNames.add(firstParam.name);
2540
3010
  }
2541
- (0, import_traverse4.default)(
3011
+ (0, import_traverse5.default)(
2542
3012
  fn.body,
2543
3013
  {
2544
3014
  noScope: true,
@@ -2580,7 +3050,7 @@ var ScreenAnalyzer = class {
2580
3050
  inferSearchField(ast, dataSource) {
2581
3051
  if (!dataSource) return void 0;
2582
3052
  let queryBinding;
2583
- (0, import_traverse4.default)(ast, {
3053
+ (0, import_traverse5.default)(ast, {
2584
3054
  CallExpression: (nodePath) => {
2585
3055
  const node = nodePath.node;
2586
3056
  if (!BabelTypes.isMemberExpression(node.callee)) return;
@@ -2591,7 +3061,7 @@ var ScreenAnalyzer = class {
2591
3061
  const fn = node.arguments[0];
2592
3062
  if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
2593
3063
  return;
2594
- (0, import_traverse4.default)(
3064
+ (0, import_traverse5.default)(
2595
3065
  fn.body,
2596
3066
  {
2597
3067
  noScope: true,
@@ -2636,43 +3106,880 @@ var ScreenAnalyzer = class {
2636
3106
  // src/analyzers/NavigationAnalyzer.ts
2637
3107
  var import_fs4 = require("fs");
2638
3108
  var import_path4 = __toESM(require("path"));
2639
- var import_fast_glob2 = __toESM(require("fast-glob"));
3109
+ var parser2 = __toESM(require("@babel/parser"));
3110
+ var import_traverse6 = __toESM(require("@babel/traverse"));
3111
+ var t8 = __toESM(require("@babel/types"));
3112
+
3113
+ // src/ast/navigation/module-graph.ts
3114
+ var import_node_fs = require("fs");
3115
+ var import_node_path = __toESM(require("path"));
2640
3116
  var parser = __toESM(require("@babel/parser"));
2641
- var import_traverse5 = __toESM(require("@babel/traverse"));
2642
- var t5 = __toESM(require("@babel/types"));
3117
+ var t7 = __toESM(require("@babel/types"));
3118
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
3119
+ var MAX_HOPS2 = 8;
3120
+ var ModuleGraph = class {
3121
+ asts = /* @__PURE__ */ new Map();
3122
+ resolved = /* @__PURE__ */ new Map();
3123
+ /** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
3124
+ aliases;
3125
+ /** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
3126
+ workspacePackages;
3127
+ constructor(rootDir) {
3128
+ this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
3129
+ this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
3130
+ }
3131
+ /** AST de um arquivo, memoizada. `null` quando não parseia. */
3132
+ parse(file2) {
3133
+ const cached = this.asts.get(file2);
3134
+ if (cached !== void 0) return cached;
3135
+ let ast = null;
3136
+ try {
3137
+ ast = parser.parse((0, import_node_fs.readFileSync)(file2, "utf-8"), {
3138
+ sourceType: "module",
3139
+ plugins: ["jsx", "typescript"]
3140
+ });
3141
+ } catch {
3142
+ ast = null;
3143
+ }
3144
+ this.asts.set(file2, ast);
3145
+ return ast;
3146
+ }
3147
+ /**
3148
+ * `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
3149
+ * Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
3150
+ * é de terceiro e não tem tela nossa dentro.
3151
+ */
3152
+ resolve(fromFile, spec) {
3153
+ const key = `${fromFile} ${spec}`;
3154
+ const cached = this.resolved.get(key);
3155
+ if (cached !== void 0) return cached;
3156
+ let base = null;
3157
+ if (spec.startsWith(".")) {
3158
+ base = import_node_path.default.resolve(import_node_path.default.dirname(fromFile), spec);
3159
+ } else {
3160
+ for (const { prefix, target } of this.aliases) {
3161
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
3162
+ base = import_node_path.default.join(target, spec.slice(prefix.length));
3163
+ break;
3164
+ }
3165
+ }
3166
+ if (!base) {
3167
+ for (const pkg of this.workspacePackages) {
3168
+ if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
3169
+ base = import_node_path.default.join(pkg.dir, spec.slice(pkg.name.length));
3170
+ break;
3171
+ }
3172
+ }
3173
+ }
3174
+ }
3175
+ if (!base) {
3176
+ this.resolved.set(key, null);
3177
+ return null;
3178
+ }
3179
+ const candidates = [
3180
+ base,
3181
+ ...EXTENSIONS.map((e) => base + e),
3182
+ ...EXTENSIONS.map((e) => import_node_path.default.join(base, "index" + e))
3183
+ ];
3184
+ let found = null;
3185
+ for (const c of candidates) {
3186
+ try {
3187
+ if ((0, import_node_fs.existsSync)(c) && (0, import_node_fs.statSync)(c).isFile()) {
3188
+ found = c;
3189
+ break;
3190
+ }
3191
+ } catch {
3192
+ }
3193
+ }
3194
+ this.resolved.set(key, found);
3195
+ return found;
3196
+ }
3197
+ /** `import`s do arquivo, por nome local. */
3198
+ imports(file2) {
3199
+ const out = /* @__PURE__ */ new Map();
3200
+ const ast = this.parse(file2);
3201
+ if (!ast) return out;
3202
+ for (const stmt of ast.program.body) {
3203
+ if (!t7.isImportDeclaration(stmt)) continue;
3204
+ const source = stmt.source.value;
3205
+ for (const spec of stmt.specifiers) {
3206
+ if (t7.isImportDefaultSpecifier(spec)) {
3207
+ out.set(spec.local.name, { source, imported: "default" });
3208
+ } else if (t7.isImportNamespaceSpecifier(spec)) {
3209
+ out.set(spec.local.name, { source, imported: "*" });
3210
+ } else if (t7.isImportSpecifier(spec)) {
3211
+ const imported = t7.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
3212
+ out.set(spec.local.name, { source, imported });
3213
+ }
3214
+ }
3215
+ }
3216
+ return out;
3217
+ }
3218
+ /** `const X = <init>` no topo do arquivo, incluindo `export const`. */
3219
+ topLevelInit(file2, name) {
3220
+ const ast = this.parse(file2);
3221
+ if (!ast) return null;
3222
+ for (const stmt of ast.program.body) {
3223
+ const decl = t7.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
3224
+ if (!t7.isVariableDeclaration(decl)) continue;
3225
+ for (const d of decl.declarations) {
3226
+ if (t7.isIdentifier(d.id) && d.id.name === name && d.init) {
3227
+ return t7.isTSAsExpression(d.init) ? d.init.expression : d.init;
3228
+ }
3229
+ }
3230
+ }
3231
+ return null;
3232
+ }
3233
+ /**
3234
+ * Onde `name` é DEFINIDO — segue import e reexport de barrel.
3235
+ * Devolve o arquivo e o nome sob o qual ele é definido lá.
3236
+ */
3237
+ resolveBinding(file2, name, hops = 0) {
3238
+ if (hops > MAX_HOPS2) return null;
3239
+ if (this.topLevelInit(file2, name) !== null) return { file: file2, name };
3240
+ const binding = this.imports(file2).get(name);
3241
+ if (binding) {
3242
+ const target = this.resolve(file2, binding.source);
3243
+ if (!target) return null;
3244
+ const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
3245
+ const deeper = this.resolveBinding(target, next, hops + 1);
3246
+ return deeper ?? { file: target, name: next };
3247
+ }
3248
+ const ast = this.parse(file2);
3249
+ if (ast) {
3250
+ for (const stmt of ast.program.body) {
3251
+ if (!t7.isExportNamedDeclaration(stmt) || !stmt.source) continue;
3252
+ for (const spec of stmt.specifiers) {
3253
+ if (!t7.isExportSpecifier(spec)) continue;
3254
+ const exported = t7.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
3255
+ if (exported !== name) continue;
3256
+ const target = this.resolve(file2, stmt.source.value);
3257
+ if (!target) return null;
3258
+ const local = spec.local.name;
3259
+ return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
3260
+ }
3261
+ }
3262
+ }
3263
+ if (ast) {
3264
+ for (const stmt of ast.program.body) {
3265
+ if (!t7.isExportAllDeclaration(stmt)) continue;
3266
+ const target = this.resolve(file2, stmt.source.value);
3267
+ if (!target || target === file2) continue;
3268
+ const deeper = this.resolveBinding(target, name, hops + 1);
3269
+ if (deeper) return deeper;
3270
+ }
3271
+ }
3272
+ if (ast) {
3273
+ for (const stmt of ast.program.body) {
3274
+ if (!t7.isExportDefaultDeclaration(stmt)) continue;
3275
+ if (t7.isIdentifier(stmt.declaration)) {
3276
+ const local = stmt.declaration.name;
3277
+ if (local === name) return null;
3278
+ return this.resolveBinding(file2, local, hops + 1) ?? { file: file2, name: local };
3279
+ }
3280
+ }
3281
+ }
3282
+ return null;
3283
+ }
3284
+ /**
3285
+ * O valor string de uma expressão de nome de rota, ou `null`.
3286
+ *
3287
+ * Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
3288
+ * seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
3289
+ * com interpolação nem valor calculado, de propósito.
3290
+ */
3291
+ stringConstant(file2, node, hops = 0) {
3292
+ if (!node || hops > MAX_HOPS2) return null;
3293
+ if (t7.isStringLiteral(node)) return node.value;
3294
+ if (t7.isTemplateLiteral(node)) {
3295
+ return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
3296
+ }
3297
+ if (t7.isTSAsExpression(node)) return this.stringConstant(file2, node.expression, hops + 1);
3298
+ const chain = memberChain(node);
3299
+ if (!chain) return null;
3300
+ const origin = this.resolveBinding(file2, chain.root);
3301
+ if (!origin) return null;
3302
+ let current = this.topLevelInit(origin.file, origin.name);
3303
+ if (!current) return null;
3304
+ for (const key of chain.path) {
3305
+ if (!t7.isObjectExpression(current)) return null;
3306
+ const prop = objectProperty(current, key);
3307
+ if (!prop) return null;
3308
+ current = t7.isTSAsExpression(prop) ? prop.expression : prop;
3309
+ }
3310
+ return t7.isStringLiteral(current) ? current.value : null;
3311
+ }
3312
+ /**
3313
+ * O ARQUIVO onde vive o componente de uma rota, ou `null`.
3314
+ *
3315
+ * Aceita as três formas que o corpus mostrou: identificador
3316
+ * (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
3317
+ * e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
3318
+ * que é como o `pocketpal` monta todas as telas do Drawer).
3319
+ */
3320
+ componentFile(file2, node, hops = 0) {
3321
+ if (!node || hops > MAX_HOPS2) return null;
3322
+ if (t7.isCallExpression(node)) {
3323
+ for (const arg of node.arguments) {
3324
+ if (t7.isIdentifier(arg) || t7.isMemberExpression(arg)) {
3325
+ const inner = this.componentFile(file2, arg, hops + 1);
3326
+ if (inner) return inner;
3327
+ }
3328
+ }
3329
+ return null;
3330
+ }
3331
+ if (t7.isTSAsExpression(node)) return this.componentFile(file2, node.expression, hops + 1);
3332
+ const chain = memberChain(node);
3333
+ if (!chain) return null;
3334
+ const origin = this.resolveBinding(file2, chain.root);
3335
+ if (!origin) return null;
3336
+ if (chain.path.length === 0) return origin.file;
3337
+ const init = this.topLevelInit(origin.file, origin.name);
3338
+ if (init && t7.isObjectExpression(init)) {
3339
+ const prop = objectProperty(init, chain.path[0]);
3340
+ if (prop) return this.componentFile(origin.file, prop, hops + 1);
3341
+ }
3342
+ const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
3343
+ return viaExport?.file ?? null;
3344
+ }
3345
+ /**
3346
+ * Os arquivos DO PROJETO que este arquivo renderiza como JSX.
3347
+ *
3348
+ * `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
3349
+ * Import de pacote devolve `null` no `resolve` e fica de fora: componente de
3350
+ * terceiro não tem tela nossa dentro.
3351
+ *
3352
+ * Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
3353
+ * extractor, e um navegador não é conteúdo de tela.
3354
+ */
3355
+ renderedComponentFiles(file2) {
3356
+ const ast = this.parse(file2);
3357
+ if (!ast) return [];
3358
+ const names = /* @__PURE__ */ new Set();
3359
+ const visit = (node) => {
3360
+ if (!node || typeof node !== "object") return;
3361
+ if (t7.isJSXOpeningElement(node) && t7.isJSXIdentifier(node.name)) {
3362
+ const n = node.name.name;
3363
+ if (/^[A-Z]/.test(n)) names.add(n);
3364
+ }
3365
+ for (const key of Object.keys(node)) {
3366
+ const value = node[key];
3367
+ if (Array.isArray(value)) {
3368
+ for (const item of value) visit(item);
3369
+ } else if (value && typeof value === "object" && "type" in value) {
3370
+ visit(value);
3371
+ }
3372
+ }
3373
+ };
3374
+ visit(ast.program);
3375
+ const out = /* @__PURE__ */ new Set();
3376
+ for (const name of names) {
3377
+ const origin = this.resolveBinding(file2, name);
3378
+ if (origin && origin.file !== file2) out.add(origin.file);
3379
+ }
3380
+ return [...out];
3381
+ }
3382
+ };
3383
+ function memberChain(node) {
3384
+ const chain = [];
3385
+ let current = node;
3386
+ while (t7.isMemberExpression(current)) {
3387
+ if (current.computed) {
3388
+ if (!t7.isStringLiteral(current.property)) return null;
3389
+ chain.unshift(current.property.value);
3390
+ } else if (t7.isIdentifier(current.property)) {
3391
+ chain.unshift(current.property.name);
3392
+ } else {
3393
+ return null;
3394
+ }
3395
+ current = current.object;
3396
+ }
3397
+ return t7.isIdentifier(current) ? { root: current.name, path: chain } : null;
3398
+ }
3399
+ function objectProperty(obj, key) {
3400
+ for (const prop of obj.properties) {
3401
+ if (!t7.isObjectProperty(prop)) continue;
3402
+ const name = t7.isIdentifier(prop.key) ? prop.key.name : t7.isStringLiteral(prop.key) ? prop.key.value : null;
3403
+ if (name === key && t7.isExpression(prop.value)) return prop.value;
3404
+ }
3405
+ return null;
3406
+ }
3407
+ function readTsconfigAliases(rootDir) {
3408
+ const file2 = import_node_path.default.join(rootDir, "tsconfig.json");
3409
+ if (!(0, import_node_fs.existsSync)(file2)) return [];
3410
+ let parsed;
3411
+ try {
3412
+ const raw = stripJsonComments((0, import_node_fs.readFileSync)(file2, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
3413
+ parsed = JSON.parse(raw);
3414
+ } catch {
3415
+ return [];
3416
+ }
3417
+ const paths = parsed.compilerOptions?.paths;
3418
+ if (!paths) return [];
3419
+ const baseUrl = import_node_path.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
3420
+ const out = [];
3421
+ for (const [pattern, targets] of Object.entries(paths)) {
3422
+ const first = targets[0];
3423
+ if (typeof first !== "string") continue;
3424
+ out.push({
3425
+ prefix: pattern.replace(/\/?\*$/, ""),
3426
+ target: import_node_path.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
3427
+ });
3428
+ }
3429
+ return out.sort((a, b) => b.prefix.length - a.prefix.length);
3430
+ }
3431
+ function stripJsonComments(text) {
3432
+ let out = "";
3433
+ let inString = false;
3434
+ let escaped = false;
3435
+ for (let i = 0; i < text.length; i++) {
3436
+ const c = text[i];
3437
+ if (inString) {
3438
+ out += c;
3439
+ if (escaped) escaped = false;
3440
+ else if (c === "\\") escaped = true;
3441
+ else if (c === '"') inString = false;
3442
+ continue;
3443
+ }
3444
+ if (c === '"') {
3445
+ inString = true;
3446
+ out += c;
3447
+ continue;
3448
+ }
3449
+ if (c === "/" && text[i + 1] === "/") {
3450
+ while (i < text.length && text[i] !== "\n") i++;
3451
+ out += "\n";
3452
+ continue;
3453
+ }
3454
+ if (c === "/" && text[i + 1] === "*") {
3455
+ i += 2;
3456
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
3457
+ i++;
3458
+ continue;
3459
+ }
3460
+ out += c;
3461
+ }
3462
+ return out;
3463
+ }
3464
+ function readWorkspacePackages(rootDir) {
3465
+ const start = import_node_path.default.resolve(rootDir);
3466
+ let dir = start;
3467
+ for (let i = 0; i < 6; i++) {
3468
+ const globs = workspaceGlobs(dir);
3469
+ if (globs.length > 0) {
3470
+ const packages = expandWorkspaceGlobs(dir, globs);
3471
+ const containsApp = packages.some(
3472
+ (p) => start === p.dir || start.startsWith(p.dir + import_node_path.default.sep)
3473
+ );
3474
+ return containsApp ? packages : [];
3475
+ }
3476
+ const parent = import_node_path.default.dirname(dir);
3477
+ if (parent === dir) break;
3478
+ dir = parent;
3479
+ }
3480
+ return [];
3481
+ }
3482
+ function workspaceGlobs(dir) {
3483
+ const pnpm = import_node_path.default.join(dir, "pnpm-workspace.yaml");
3484
+ if ((0, import_node_fs.existsSync)(pnpm)) {
3485
+ try {
3486
+ const lines = (0, import_node_fs.readFileSync)(pnpm, "utf-8").split(/\r?\n/);
3487
+ const out = [];
3488
+ let inPackages = false;
3489
+ for (const line of lines) {
3490
+ if (/^packages:/.test(line)) {
3491
+ inPackages = true;
3492
+ continue;
3493
+ }
3494
+ if (inPackages) {
3495
+ const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
3496
+ if (m) out.push(m[1].trim());
3497
+ else if (/^\S/.test(line)) break;
3498
+ }
3499
+ }
3500
+ if (out.length) return out;
3501
+ } catch {
3502
+ }
3503
+ }
3504
+ const pkgPath = import_node_path.default.join(dir, "package.json");
3505
+ if (!(0, import_node_fs.existsSync)(pkgPath)) return [];
3506
+ try {
3507
+ const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8"));
3508
+ const ws = pkg.workspaces;
3509
+ if (Array.isArray(ws)) return ws;
3510
+ if (ws && Array.isArray(ws.packages)) return ws.packages;
3511
+ } catch {
3512
+ }
3513
+ return [];
3514
+ }
3515
+ function expandWorkspaceGlobs(root, globs) {
3516
+ const out = [];
3517
+ const add2 = (dir) => {
3518
+ const pkgPath = import_node_path.default.join(dir, "package.json");
3519
+ if (!(0, import_node_fs.existsSync)(pkgPath)) return;
3520
+ try {
3521
+ const name = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf-8")).name;
3522
+ if (name) out.push({ name, dir });
3523
+ } catch {
3524
+ }
3525
+ };
3526
+ for (const glob2 of globs) {
3527
+ if (glob2.endsWith("/*")) {
3528
+ const parent = import_node_path.default.join(root, glob2.slice(0, -2));
3529
+ let entries = [];
3530
+ try {
3531
+ entries = (0, import_node_fs.readdirSync)(parent);
3532
+ } catch {
3533
+ continue;
3534
+ }
3535
+ for (const entry of entries) {
3536
+ const dir = import_node_path.default.join(parent, entry);
3537
+ try {
3538
+ if ((0, import_node_fs.statSync)(dir).isDirectory()) add2(dir);
3539
+ } catch {
3540
+ }
3541
+ }
3542
+ } else if (!glob2.includes("*")) {
3543
+ add2(import_node_path.default.join(root, glob2));
3544
+ }
3545
+ }
3546
+ return out.sort((a, b) => b.name.length - a.name.length);
3547
+ }
3548
+
3549
+ // src/ast/navigation/expo-router.ts
3550
+ var import_node_fs2 = require("fs");
3551
+ var import_node_path2 = __toESM(require("path"));
3552
+ var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
3553
+ var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
3554
+ var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
3555
+ function baseName(file2) {
3556
+ let name = file2;
3557
+ for (const ext of ROUTE_EXTENSIONS) {
3558
+ if (name.endsWith(ext)) {
3559
+ name = name.slice(0, -ext.length);
3560
+ break;
3561
+ }
3562
+ }
3563
+ for (const suffix of PLATFORM_SUFFIXES) {
3564
+ if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
3565
+ }
3566
+ return name;
3567
+ }
3568
+ function isRouteFile(file2) {
3569
+ return ROUTE_EXTENSIONS.some((ext) => file2.endsWith(ext));
3570
+ }
3571
+ function isSpecial(base) {
3572
+ if (base === "_layout") return true;
3573
+ if (base.startsWith("+")) return base !== "+not-found";
3574
+ return base.startsWith("_");
3575
+ }
3576
+ function layoutType(file2) {
3577
+ try {
3578
+ const src = (0, import_node_fs2.readFileSync)(file2, "utf-8");
3579
+ if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
3580
+ if (/<Drawer\b/.test(src)) return "drawer";
3581
+ } catch {
3582
+ }
3583
+ return "stack";
3584
+ }
3585
+ function findLayout(dir) {
3586
+ for (const ext of ROUTE_EXTENSIONS) {
3587
+ const candidate = import_node_path2.default.join(dir, "_layout" + ext);
3588
+ if ((0, import_node_fs2.existsSync)(candidate)) return candidate;
3589
+ }
3590
+ return null;
3591
+ }
3592
+ function analyzeExpoRouter(appRoot) {
3593
+ let routeDir = null;
3594
+ for (const candidate of ROUTE_DIR_CANDIDATES) {
3595
+ const full = import_node_path2.default.join(appRoot, candidate);
3596
+ if ((0, import_node_fs2.existsSync)(full) && findLayout(full)) {
3597
+ routeDir = full;
3598
+ break;
3599
+ }
3600
+ }
3601
+ if (!routeDir) return null;
3602
+ const routes = [];
3603
+ const navigators = /* @__PURE__ */ new Map();
3604
+ const seen = /* @__PURE__ */ new Set();
3605
+ const walk = (dir, segments, navigator) => {
3606
+ const layout = findLayout(dir);
3607
+ const current = layout ? segments.join("/") || "/" : navigator;
3608
+ if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
3609
+ let entries;
3610
+ try {
3611
+ entries = (0, import_node_fs2.readdirSync)(dir);
3612
+ } catch {
3613
+ return;
3614
+ }
3615
+ for (const entry of entries.sort()) {
3616
+ const full = import_node_path2.default.join(dir, entry);
3617
+ let isDir = false;
3618
+ try {
3619
+ isDir = (0, import_node_fs2.statSync)(full).isDirectory();
3620
+ } catch {
3621
+ continue;
3622
+ }
3623
+ if (isDir) {
3624
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
3625
+ walk(full, [...segments, entry], current);
3626
+ continue;
3627
+ }
3628
+ if (!isRouteFile(entry)) continue;
3629
+ const base = baseName(entry);
3630
+ if (isSpecial(base)) continue;
3631
+ const routeSegments = base === "index" ? segments : [...segments, base];
3632
+ const name = "/" + routeSegments.join("/");
3633
+ if (seen.has(name)) continue;
3634
+ seen.add(name);
3635
+ routes.push({
3636
+ name,
3637
+ componentFile: full,
3638
+ navigatorName: current,
3639
+ navigatorType: navigators.get(current) ?? "stack",
3640
+ params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
3641
+ });
3642
+ }
3643
+ };
3644
+ walk(routeDir, [], "/");
3645
+ return {
3646
+ routeDir: import_node_path2.default.relative(appRoot, routeDir),
3647
+ routes,
3648
+ navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
3649
+ };
3650
+ }
3651
+
3652
+ // src/analyzers/NavigationAnalyzer.ts
3653
+ var NAVIGATOR_FACTORIES = {
3654
+ createStackNavigator: "stack",
3655
+ createNativeStackNavigator: "stack",
3656
+ createBottomTabNavigator: "tab",
3657
+ createTabNavigator: "tab",
3658
+ createMaterialTopTabNavigator: "tab",
3659
+ createMaterialBottomTabNavigator: "tab",
3660
+ createDrawerNavigator: "drawer"
3661
+ };
3662
+ var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
3663
+ function unwrapStaticScreen(value) {
3664
+ if (t8.isCallExpression(value)) {
3665
+ const arg = value.arguments[0];
3666
+ return arg ? unwrapStaticScreen(arg) : null;
3667
+ }
3668
+ if (t8.isObjectExpression(value)) {
3669
+ for (const prop of value.properties) {
3670
+ if (!t8.isObjectProperty(prop)) continue;
3671
+ const key = t8.isIdentifier(prop.key) ? prop.key.name : null;
3672
+ if (key === "screen" && t8.isExpression(prop.value)) return prop.value;
3673
+ }
3674
+ return null;
3675
+ }
3676
+ return t8.isExpression(value) ? value : null;
3677
+ }
3678
+ function inferNavigatorType(factoryName) {
3679
+ if (/drawer/i.test(factoryName)) return "drawer";
3680
+ if (/tab/i.test(factoryName)) return "tab";
3681
+ return "stack";
3682
+ }
3683
+ var NAVIGATION_EVIDENCE = new RegExp(
3684
+ // `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
3685
+ // não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
3686
+ // `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
3687
+ // que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
3688
+ // junção entre arquivos nunca chega a ser tentada.
3689
+ // Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
3690
+ // fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
3691
+ // seis navegadores do `bluesky`.
3692
+ `create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
3693
+ );
2643
3694
  var NavigationAnalyzer = class {
2644
3695
  config;
2645
3696
  /** Extra glob patterns for navigation file discovery (added to defaults) */
2646
3697
  navigationInclude;
2647
3698
  /** Extra glob patterns to exclude from navigation analysis */
2648
3699
  navigationExclude;
3700
+ /** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
3701
+ graph;
3702
+ /**
3703
+ * Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
3704
+ * de navegação e a árvore de telas; sem ela as duas metades do documento
3705
+ * falam de coisas diferentes. Populada por `analyze()`.
3706
+ */
3707
+ routeTargets = /* @__PURE__ */ new Map();
3708
+ /**
3709
+ * Arquivos que DECLARAM uma fábrica de navegador.
3710
+ *
3711
+ * Um navegador aninhado é montado como `component=` de uma rota — no
3712
+ * `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
3713
+ * Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
3714
+ * exatamente o falso positivo que o corpus existe para não repetir. Um
3715
+ * navegador é um contêiner de rotas; a tela está um nível abaixo.
3716
+ *
3717
+ * Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
3718
+ * RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
3719
+ * `DetailViewStack` num arquivo e escreve o JSX em
3720
+ * `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
3721
+ * segundo entrar como tela.
3722
+ */
3723
+ navigatorFiles = /* @__PURE__ */ new Set();
3724
+ /** Preenchido por `analyze()`. Vazio antes disso. */
3725
+ diagnostics = {
3726
+ filesScanned: 0,
3727
+ filesWithEvidence: 0,
3728
+ navigatorsDeclared: 0,
3729
+ navigatorsWithJsx: 0,
3730
+ routes: 0,
3731
+ routesLinkedToFile: 0
3732
+ };
2649
3733
  constructor(config, options) {
2650
3734
  this.config = config;
3735
+ this.graph = new ModuleGraph(config.rootDir);
2651
3736
  this.navigationInclude = options?.navigationInclude ?? [];
2652
3737
  this.navigationExclude = options?.navigationExclude ?? [];
2653
3738
  }
2654
3739
  /** Build the full navigation graph */
2655
3740
  async analyze() {
2656
3741
  const navigationFiles = await this.findNavigationFiles();
2657
- const parsedNavigators = [];
2658
3742
  const typeExports = [];
3743
+ this.routeTargets.clear();
3744
+ const declarations = /* @__PURE__ */ new Map();
3745
+ const evidenceFiles = [];
3746
+ this.navigatorFiles.clear();
2659
3747
  for (const filePath of navigationFiles) {
2660
3748
  try {
2661
3749
  const content = await import_fs4.promises.readFile(filePath, "utf-8");
2662
- const navigators = this.parseNavigators(content, filePath);
2663
- const types = this.parseParamTypes(content, filePath);
2664
- parsedNavigators.push(...navigators);
2665
- typeExports.push(...types);
3750
+ if (!NAVIGATION_EVIDENCE.test(content)) continue;
3751
+ evidenceFiles.push(filePath);
3752
+ for (const decl of this.collectDeclarations(filePath)) {
3753
+ declarations.set(`${decl.file}#${decl.name}`, decl);
3754
+ if (decl.confident) this.navigatorFiles.add(decl.file);
3755
+ }
3756
+ typeExports.push(...this.parseParamTypes(content, filePath));
2666
3757
  } catch (error2) {
2667
3758
  console.warn(`Failed to parse ${filePath}:`, error2);
2668
3759
  }
2669
3760
  }
3761
+ const parsedNavigators = [];
3762
+ for (const filePath of evidenceFiles) {
3763
+ try {
3764
+ parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
3765
+ } catch (error2) {
3766
+ console.warn(`Failed to parse navigators in ${filePath}:`, error2);
3767
+ }
3768
+ }
3769
+ let detachedScreens = 0;
3770
+ for (const filePath of evidenceFiles) {
3771
+ try {
3772
+ for (const detached of this.parseDetachedScreens(filePath, declarations)) {
3773
+ const existing = parsedNavigators.find(
3774
+ (nav) => nav.name === detached.name && nav.type === detached.type
3775
+ );
3776
+ if (!existing) {
3777
+ parsedNavigators.push(detached);
3778
+ detachedScreens += detached.screens.length;
3779
+ continue;
3780
+ }
3781
+ const seen = new Set(existing.screens.map((screen) => screen.name));
3782
+ for (const screen of detached.screens) {
3783
+ if (seen.has(screen.name)) continue;
3784
+ seen.add(screen.name);
3785
+ existing.screens.push(screen);
3786
+ detachedScreens++;
3787
+ }
3788
+ }
3789
+ } catch (error2) {
3790
+ console.warn(`Failed to parse detached screens in ${filePath}:`, error2);
3791
+ }
3792
+ }
3793
+ const expo = analyzeExpoRouter(this.config.rootDir);
3794
+ if (expo) {
3795
+ console.log(
3796
+ `[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
3797
+ );
3798
+ this.diagnostics.fileBasedRouter = {
3799
+ kind: "expo-router",
3800
+ routeDir: expo.routeDir,
3801
+ routes: expo.routes.length
3802
+ };
3803
+ const byNavigator = /* @__PURE__ */ new Map();
3804
+ for (const nav of expo.navigators) {
3805
+ byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
3806
+ }
3807
+ for (const route of expo.routes) {
3808
+ const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
3809
+ name: route.navigatorName,
3810
+ type: route.navigatorType,
3811
+ screens: []
3812
+ }).get(route.navigatorName);
3813
+ navigator.screens.push({
3814
+ name: route.name,
3815
+ navigatorName: route.navigatorName,
3816
+ navigatorType: route.navigatorType,
3817
+ componentFile: route.componentFile,
3818
+ ...route.params.length ? {
3819
+ params: route.params.map((name) => ({ name, type: "string", required: true }))
3820
+ } : {}
3821
+ });
3822
+ this.routeTargets.set(route.name, route.componentFile);
3823
+ }
3824
+ parsedNavigators.push(...byNavigator.values());
3825
+ }
3826
+ let staticOnly = 0;
3827
+ for (const decl of declarations.values()) {
3828
+ if (!decl.confident || !decl.staticScreens?.length) continue;
3829
+ const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
3830
+ if (existing) {
3831
+ const seen = new Set(existing.screens.map((s) => s.name));
3832
+ for (const screen of decl.staticScreens) {
3833
+ if (!seen.has(screen.name)) existing.screens.push(screen);
3834
+ }
3835
+ continue;
3836
+ }
3837
+ parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
3838
+ staticOnly++;
3839
+ }
3840
+ const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
3841
+ const uniqueRoutes = new Set(
3842
+ parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
3843
+ );
3844
+ this.diagnostics = {
3845
+ ...this.diagnostics,
3846
+ filesScanned: navigationFiles.length,
3847
+ filesWithEvidence: evidenceFiles.length,
3848
+ navigatorsDeclared: confidentDeclarations,
3849
+ navigatorsWithJsx: parsedNavigators.length,
3850
+ routes: uniqueRoutes.size,
3851
+ // `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
3852
+ // registrada por um navegador que não sobreviveu. O mínimo evita a
3853
+ // cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
3854
+ routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
3855
+ };
3856
+ console.log(
3857
+ `[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`
3858
+ );
2670
3859
  this.attachParamsToNavigators(parsedNavigators, typeExports);
2671
3860
  return this.buildNavigationGraph(parsedNavigators);
2672
3861
  }
2673
- /** Find all navigation-related files */
3862
+ /** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
3863
+ collectDeclarations(filePath) {
3864
+ const ast = this.graph.parse(filePath);
3865
+ if (!ast) return [];
3866
+ const out = /* @__PURE__ */ new Map();
3867
+ (0, import_traverse6.default)(ast, {
3868
+ CallExpression: (nodePath) => {
3869
+ const callee = nodePath.node.callee;
3870
+ if (!t8.isIdentifier(callee)) return;
3871
+ const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
3872
+ const type = known ?? inferNavigatorType(callee.name);
3873
+ const confident = known !== null;
3874
+ let up = nodePath.parentPath;
3875
+ for (let i = 0; i < 4 && up; i++) {
3876
+ if (t8.isVariableDeclarator(up.node)) break;
3877
+ if (!t8.isMemberExpression(up.node) && !t8.isCallExpression(up.node)) {
3878
+ up = null;
3879
+ break;
3880
+ }
3881
+ up = up.parentPath;
3882
+ }
3883
+ const declarator = up?.node;
3884
+ if (!declarator || !t8.isVariableDeclarator(declarator) || !t8.isIdentifier(declarator.id)) {
3885
+ return;
3886
+ }
3887
+ const name = declarator.id.name;
3888
+ if (out.get(name)?.confident) return;
3889
+ const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
3890
+ out.set(name, {
3891
+ file: filePath,
3892
+ name,
3893
+ type,
3894
+ confident,
3895
+ ...staticScreens.length ? { staticScreens } : {}
3896
+ });
3897
+ }
3898
+ });
3899
+ return [...out.values()];
3900
+ }
3901
+ /**
3902
+ * As rotas de `createXNavigator({ screens: { … } })`.
3903
+ *
3904
+ * Quatro formas no corpus, todas em `rocketchat`:
3905
+ * `NewServerView` — shorthand, nome = componente
3906
+ * `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
3907
+ * `SelectListView: SelectListViewScreen` — identificador direto
3908
+ * `ChatsStackNavigator: ChatsStack` — navegador aninhado
3909
+ *
3910
+ * `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
3911
+ * e agrupar rotas não muda o que elas são.
3912
+ */
3913
+ parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
3914
+ if (!config || !t8.isObjectExpression(config) || depth > 4) return [];
3915
+ const out = [];
3916
+ for (const prop of config.properties) {
3917
+ if (!t8.isObjectProperty(prop)) continue;
3918
+ const key = t8.isIdentifier(prop.key) ? prop.key.name : t8.isStringLiteral(prop.key) ? prop.key.value : null;
3919
+ if (!key) continue;
3920
+ if (key === "groups" && t8.isObjectExpression(prop.value)) {
3921
+ for (const group of prop.value.properties) {
3922
+ if (!t8.isObjectProperty(group) || !t8.isExpression(group.value)) continue;
3923
+ out.push(
3924
+ ...this.parseStaticScreens(
3925
+ filePath,
3926
+ group.value,
3927
+ navigatorName,
3928
+ navigatorType,
3929
+ depth + 1
3930
+ )
3931
+ );
3932
+ }
3933
+ continue;
3934
+ }
3935
+ if (key !== "screens" || !t8.isObjectExpression(prop.value)) continue;
3936
+ for (const entry of prop.value.properties) {
3937
+ if (!t8.isObjectProperty(entry)) continue;
3938
+ const routeName = t8.isIdentifier(entry.key) ? entry.key.name : t8.isStringLiteral(entry.key) ? entry.key.value : null;
3939
+ if (!routeName) continue;
3940
+ const componentExpr = unwrapStaticScreen(entry.value);
3941
+ const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
3942
+ if (componentFile) this.routeTargets.set(routeName, componentFile);
3943
+ out.push({
3944
+ name: routeName,
3945
+ navigatorName,
3946
+ navigatorType,
3947
+ ...componentFile ? { componentFile } : {}
3948
+ });
3949
+ }
3950
+ }
3951
+ return out;
3952
+ }
3953
+ /**
3954
+ * Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
3955
+ * certo.
3956
+ *
3957
+ * POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
3958
+ * `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
3959
+ * convenção de caminho. Medido contra 20 apps React Native de terceiros
3960
+ * (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
3961
+ * app não ter navegação:
3962
+ *
3963
+ * - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
3964
+ * - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
3965
+ * sensível a caixa;
3966
+ * - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
3967
+ * pedia um arquivo chamado `routes*`;
3968
+ * - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
3969
+ * - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
3970
+ *
3971
+ * Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
3972
+ * globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
3973
+ * O que segura o custo aqui é o portão por evidência em `analyze()`, que só
3974
+ * parseia arquivo cujo texto menciona uma fábrica de navegador.
3975
+ *
3976
+ * Os globs antigos continuam na lista: se um projeto restringir `include`, o
3977
+ * que era encontrado antes continua sendo.
3978
+ */
2674
3979
  async findNavigationFiles() {
2675
3980
  const patterns = [
3981
+ ...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
3982
+ // Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
2676
3983
  "**/navigation/**/*.{ts,tsx}",
2677
3984
  "**/navigator*.{ts,tsx}",
2678
3985
  "**/routes*.{ts,tsx}",
@@ -2683,91 +3990,188 @@ var NavigationAnalyzer = class {
2683
3990
  "**/node_modules/**",
2684
3991
  "**/dist/**",
2685
3992
  "**/build/**",
3993
+ // Um navegador declarado dentro de um teste é fixture, não a navegação
3994
+ // do app. Isto passou a importar quando a varredura deixou de ser por
3995
+ // caminho: em `expensify`, os únicos arquivos que o glob antigo
3996
+ // alcançava eram três de `tests/`.
3997
+ "**/__tests__/**",
3998
+ "**/test/**",
3999
+ "**/tests/**",
4000
+ "**/*.test.{ts,tsx,js,jsx}",
4001
+ "**/*.tests.{ts,tsx,js,jsx}",
4002
+ "**/*.spec.{ts,tsx,js,jsx}",
4003
+ // Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
4004
+ // `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
4005
+ // Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
4006
+ // destinos que existem no teste e não no app, para os quais o agente
4007
+ // tentaria navegar.
4008
+ "**/*Test.{ts,tsx,js,jsx}",
4009
+ "**/*Tests.{ts,tsx,js,jsx}",
2686
4010
  ...this.config.exclude || [],
2687
4011
  // §5: Add user-configured navigation excludes
2688
4012
  ...this.navigationExclude
2689
4013
  ];
2690
- const files = await (0, import_fast_glob2.default)(patterns, {
4014
+ const files = await globSorted(patterns, {
2691
4015
  cwd: this.config.rootDir,
2692
4016
  ignore: excludePatterns
2693
4017
  });
2694
4018
  return files.map((file2) => import_path4.default.join(this.config.rootDir, file2));
2695
4019
  }
2696
- /** Parse navigator definitions from a file */
2697
- parseNavigators(content, filePath) {
2698
- const navigators = [];
2699
- try {
2700
- const ast = parser.parse(content, {
2701
- sourceType: "module",
2702
- plugins: [
2703
- "jsx",
2704
- "typescript",
2705
- ...this.config.parserPlugins || []
2706
- ]
2707
- });
2708
- const navigatorCalls = /* @__PURE__ */ new Map();
2709
- const screensByNavigator = /* @__PURE__ */ new Map();
2710
- (0, import_traverse5.default)(ast, {
2711
- // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
2712
- CallExpression: (nodePath) => {
2713
- const { node } = nodePath;
2714
- const callee = node.callee;
2715
- let navigatorType = null;
2716
- if (t5.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
2717
- navigatorType = "stack";
2718
- } else if (t5.isIdentifier(callee) && callee.name === "createStackNavigator") {
2719
- navigatorType = "stack";
2720
- } else if (t5.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
2721
- navigatorType = "tab";
2722
- } else if (t5.isIdentifier(callee) && callee.name === "createTabNavigator") {
2723
- navigatorType = "tab";
2724
- } else if (t5.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
2725
- navigatorType = "drawer";
2726
- }
2727
- if (navigatorType) {
2728
- const parent = nodePath.parent;
2729
- if (t5.isVariableDeclarator(parent) && t5.isIdentifier(parent.id)) {
2730
- const varName = parent.id.name;
2731
- navigatorCalls.set(varName, {
2732
- name: varName,
2733
- type: navigatorType,
2734
- screens: []
2735
- });
2736
- }
2737
- }
2738
- },
2739
- // Detect Stack.Navigator / Tab.Navigator JSX elements
2740
- JSXElement: (nodePath) => {
2741
- const { node } = nodePath;
2742
- const openingElement = node.openingElement;
2743
- if (t5.isJSXMemberExpression(openingElement.name) && t5.isJSXIdentifier(openingElement.name.object)) {
2744
- const objectName = openingElement.name.object.name;
2745
- const propertyName = t5.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2746
- if (propertyName === "Navigator") {
2747
- const navigator = navigatorCalls.get(objectName);
2748
- if (navigator) {
2749
- const initialRouteAttr = openingElement.attributes.find(
2750
- (attr) => t5.isJSXAttribute(attr) && t5.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2751
- );
2752
- if (t5.isJSXAttribute(initialRouteAttr) && t5.isStringLiteral(initialRouteAttr.value)) {
2753
- navigator.initialRouteName = initialRouteAttr.value.value;
2754
- }
2755
- const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
2756
- screensByNavigator.set(objectName, screens);
2757
- }
2758
- }
2759
- }
4020
+ /**
4021
+ * Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
4022
+ * que pode estar aqui ou em qualquer arquivo que este importe.
4023
+ */
4024
+ parseNavigatorUsages(filePath, declarations) {
4025
+ const ast = this.graph.parse(filePath);
4026
+ if (!ast) return [];
4027
+ const found = /* @__PURE__ */ new Map();
4028
+ (0, import_traverse6.default)(ast, {
4029
+ JSXElement: (nodePath) => {
4030
+ const { node } = nodePath;
4031
+ const openingElement = node.openingElement;
4032
+ if (!t8.isJSXMemberExpression(openingElement.name) || !t8.isJSXIdentifier(openingElement.name.object)) {
4033
+ return;
2760
4034
  }
2761
- });
2762
- navigatorCalls.forEach((navigator) => {
2763
- const screens = screensByNavigator.get(navigator.name) || [];
2764
- navigator.screens = screens;
2765
- navigators.push(navigator);
2766
- });
2767
- } catch (error2) {
2768
- console.warn(`Failed to parse navigators in ${filePath}:`, error2);
4035
+ const objectName = openingElement.name.object.name;
4036
+ const propertyName = t8.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
4037
+ if (propertyName !== "Navigator") return;
4038
+ const decl = this.resolveNavigator(filePath, objectName, declarations);
4039
+ if (!decl) return;
4040
+ const navigator = found.get(objectName) ?? {
4041
+ name: objectName,
4042
+ type: decl.type,
4043
+ screens: []
4044
+ };
4045
+ const initialRouteAttr = openingElement.attributes.find(
4046
+ (attr) => t8.isJSXAttribute(attr) && t8.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
4047
+ );
4048
+ if (t8.isJSXAttribute(initialRouteAttr)) {
4049
+ const initial = this.attributeString(filePath, initialRouteAttr);
4050
+ if (initial) navigator.initialRouteName = initial;
4051
+ }
4052
+ this.navigatorFiles.add(filePath);
4053
+ this.navigatorFiles.add(decl.file);
4054
+ navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
4055
+ found.set(objectName, navigator);
4056
+ }
4057
+ });
4058
+ return [...found.values()];
4059
+ }
4060
+ /**
4061
+ * Rotas declaradas FORA de qualquer `<X.Navigator>`.
4062
+ *
4063
+ * O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
4064
+ * que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
4065
+ * dois são 171 rotas invisíveis:
4066
+ *
4067
+ * `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
4068
+ * <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
4069
+ * dentro de seis navegadores diferentes. 70 rotas.
4070
+ * `comapeo` — `export const createAppScreens = ({intl}) => (<>
4071
+ * <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
4072
+ * num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
4073
+ *
4074
+ * A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
4075
+ * navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
4076
+ * literal do fonte (ou constante que o resolvedor segue), e `X` precisa
4077
+ * chegar a uma declaração de navegador que já existe. O que não resolve não
4078
+ * vira nada.
4079
+ *
4080
+ * O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
4081
+ * fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
4082
+ *
4083
+ * DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
4084
+ * primeira é a de sempre (declaração local, ou `import` seguido até a
4085
+ * origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
4086
+ * parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
4087
+ * o navegador `Flat` é o próprio app, no fonte.
4088
+ *
4089
+ * O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
4090
+ * contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
4091
+ * cinco stacks que também chamam a mesma função. Seguir os seis pontos de
4092
+ * chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
4093
+ * da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
4094
+ */
4095
+ parseDetachedScreens(filePath, declarations) {
4096
+ const ast = this.graph.parse(filePath);
4097
+ if (!ast) return [];
4098
+ const byNavigator = /* @__PURE__ */ new Map();
4099
+ const resolved = /* @__PURE__ */ new Map();
4100
+ (0, import_traverse6.default)(ast, {
4101
+ JSXElement: (nodePath) => {
4102
+ const name = nodePath.node.openingElement.name;
4103
+ if (!t8.isJSXMemberExpression(name) || !t8.isJSXIdentifier(name.object)) return;
4104
+ if (!t8.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
4105
+ const insideNavigator = nodePath.findParent((parent) => {
4106
+ if (!parent.isJSXElement()) return false;
4107
+ const parentName = parent.node.openingElement.name;
4108
+ return t8.isJSXMemberExpression(parentName) && t8.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
4109
+ });
4110
+ if (insideNavigator) return;
4111
+ const local = name.object.name;
4112
+ if (!resolved.has(local)) {
4113
+ resolved.set(
4114
+ local,
4115
+ this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
4116
+ );
4117
+ }
4118
+ const decl = resolved.get(local);
4119
+ if (!decl) return;
4120
+ const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
4121
+ if (!screen) return;
4122
+ const nav = byNavigator.get(decl.name) ?? {
4123
+ name: decl.name,
4124
+ type: decl.type,
4125
+ screens: []
4126
+ };
4127
+ if (!nav.screens.some((existing) => existing.name === screen.name))
4128
+ nav.screens.push(screen);
4129
+ byNavigator.set(decl.name, nav);
4130
+ }
4131
+ });
4132
+ return [...byNavigator.values()];
4133
+ }
4134
+ /**
4135
+ * De um `X` usado como `<X.Screen>` até a declaração do navegador.
4136
+ *
4137
+ * Além do caminho normal — declaração local ou `import` seguido até a origem
4138
+ * — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
4139
+ * `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
4140
+ */
4141
+ resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
4142
+ const direct = this.resolveNavigator(filePath, localName, declarations);
4143
+ if (direct) return direct;
4144
+ const binding = nodePath.scope.getBinding(localName);
4145
+ if (!binding || binding.kind !== "param") return null;
4146
+ const param = binding.path.node;
4147
+ if (!t8.isIdentifier(param) || !t8.isTSTypeAnnotation(param.typeAnnotation)) return null;
4148
+ const annotation = param.typeAnnotation.typeAnnotation;
4149
+ if (!t8.isTSTypeQuery(annotation) || !t8.isIdentifier(annotation.exprName)) return null;
4150
+ return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
4151
+ }
4152
+ /**
4153
+ * De `<X.Navigator>` até a declaração de `X`.
4154
+ *
4155
+ * Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
4156
+ * é definido. Resolver pelo import é o que torna a junção entre arquivos
4157
+ * segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
4158
+ * é o arquivo de DEFINIÇÃO.
4159
+ */
4160
+ resolveNavigator(filePath, localName, declarations) {
4161
+ const local = declarations.get(`${filePath}#${localName}`);
4162
+ if (local) return local;
4163
+ const origin = this.graph.resolveBinding(filePath, localName);
4164
+ if (!origin) return null;
4165
+ return declarations.get(`${origin.file}#${origin.name}`) ?? null;
4166
+ }
4167
+ /** O valor string de um atributo JSX, resolvendo constante importada. */
4168
+ attributeString(filePath, attr) {
4169
+ const value = attr.value;
4170
+ if (t8.isStringLiteral(value)) return value.value;
4171
+ if (t8.isJSXExpressionContainer(value) && t8.isExpression(value.expression)) {
4172
+ return this.graph.stringConstant(filePath, value.expression);
2769
4173
  }
2770
- return navigators;
4174
+ return null;
2771
4175
  }
2772
4176
  /**
2773
4177
  * Is this JSX element `<navigatorVarName.MEMBER …>`?
@@ -2778,9 +4182,9 @@ var NavigationAnalyzer = class {
2778
4182
  // `never`. It is a question about the member name, not about the node
2779
4183
  // kind.
2780
4184
  isNavigatorMember(node, navigatorVarName, member) {
2781
- if (!t5.isJSXElement(node)) return false;
4185
+ if (!t8.isJSXElement(node)) return false;
2782
4186
  const name = node.openingElement.name;
2783
- return t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t5.isJSXIdentifier(name.property) && name.property.name === member;
4187
+ return t8.isJSXMemberExpression(name) && t8.isJSXIdentifier(name.object) && name.object.name === navigatorVarName && t8.isJSXIdentifier(name.property) && name.property.name === member;
2784
4188
  }
2785
4189
  /**
2786
4190
  * Flatten a navigator's JSX children into the `<X.Screen>` elements they
@@ -2811,37 +4215,37 @@ var NavigationAnalyzer = class {
2811
4215
  found.push(...this.collectScreenElements(group.children, navigatorVarName, depth + 1));
2812
4216
  return;
2813
4217
  }
2814
- if (t5.isJSXElement(node)) {
4218
+ if (t8.isJSXElement(node)) {
2815
4219
  const name = node.openingElement.name;
2816
- const isForeignNavigator = t5.isJSXMemberExpression(name) && t5.isJSXIdentifier(name.property) && name.property.name === "Navigator";
4220
+ const isForeignNavigator = t8.isJSXMemberExpression(name) && t8.isJSXIdentifier(name.property) && name.property.name === "Navigator";
2817
4221
  if (isForeignNavigator) return;
2818
4222
  found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2819
4223
  return;
2820
4224
  }
2821
- if (t5.isJSXFragment(node)) {
4225
+ if (t8.isJSXFragment(node)) {
2822
4226
  found.push(...this.collectScreenElements(node.children, navigatorVarName, depth + 1));
2823
4227
  return;
2824
4228
  }
2825
- if (t5.isJSXExpressionContainer(node)) {
4229
+ if (t8.isJSXExpressionContainer(node)) {
2826
4230
  visit(node.expression);
2827
4231
  return;
2828
4232
  }
2829
- if (t5.isConditionalExpression(node)) {
4233
+ if (t8.isConditionalExpression(node)) {
2830
4234
  visit(node.consequent);
2831
4235
  visit(node.alternate);
2832
4236
  return;
2833
4237
  }
2834
- if (t5.isLogicalExpression(node)) {
4238
+ if (t8.isLogicalExpression(node)) {
2835
4239
  visit(node.left);
2836
4240
  visit(node.right);
2837
4241
  return;
2838
4242
  }
2839
- if (t5.isCallExpression(node)) {
4243
+ if (t8.isCallExpression(node)) {
2840
4244
  for (const arg of node.arguments) {
2841
- if (t5.isArrowFunctionExpression(arg) || t5.isFunctionExpression(arg)) {
2842
- if (t5.isBlockStatement(arg.body)) {
4245
+ if (t8.isArrowFunctionExpression(arg) || t8.isFunctionExpression(arg)) {
4246
+ if (t8.isBlockStatement(arg.body)) {
2843
4247
  for (const stmt of arg.body.body) {
2844
- if (t5.isReturnStatement(stmt)) visit(stmt.argument);
4248
+ if (t8.isReturnStatement(stmt)) visit(stmt.argument);
2845
4249
  }
2846
4250
  } else {
2847
4251
  visit(arg.body);
@@ -2850,11 +4254,11 @@ var NavigationAnalyzer = class {
2850
4254
  }
2851
4255
  return;
2852
4256
  }
2853
- if (t5.isArrayExpression(node)) {
4257
+ if (t8.isArrayExpression(node)) {
2854
4258
  for (const el of node.elements) visit(el);
2855
4259
  return;
2856
4260
  }
2857
- if (t5.isTSAsExpression(node) || t5.isTSNonNullExpression(node)) {
4261
+ if (t8.isTSAsExpression(node) || t8.isTSNonNullExpression(node)) {
2858
4262
  visit(node.expression);
2859
4263
  }
2860
4264
  };
@@ -2862,28 +4266,54 @@ var NavigationAnalyzer = class {
2862
4266
  return found;
2863
4267
  }
2864
4268
  /** Extract screens from a navigator JSX element */
2865
- extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
4269
+ extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
2866
4270
  const screens = [];
2867
4271
  if (!navigatorElement.children) return screens;
2868
4272
  const seen = /* @__PURE__ */ new Set();
2869
4273
  for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
2870
- const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
2871
- if (!screenName || seen.has(screenName)) continue;
2872
- seen.add(screenName);
2873
- screens.push({
2874
- name: screenName,
2875
- navigatorName: navigatorVarName,
2876
- navigatorType
2877
- });
4274
+ const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
4275
+ if (!screen || seen.has(screen.name)) continue;
4276
+ seen.add(screen.name);
4277
+ screens.push(screen);
2878
4278
  }
2879
4279
  return screens;
2880
4280
  }
4281
+ /**
4282
+ * Uma `<X.Screen>` isolada até a rota que ela declara.
4283
+ *
4284
+ * Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
4285
+ * a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
4286
+ * dele — só a forma de chegar até o elemento muda.
4287
+ */
4288
+ parseScreenElement(filePath, element, navigatorName, navigatorType) {
4289
+ const attrs = element.openingElement.attributes;
4290
+ const nameAttr = attrs.find(
4291
+ (a) => t8.isJSXAttribute(a) && t8.isJSXIdentifier(a.name) && a.name.name === "name"
4292
+ );
4293
+ const screenName = t8.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
4294
+ if (!screenName) return null;
4295
+ const componentAttr = attrs.find(
4296
+ (a) => t8.isJSXAttribute(a) && t8.isJSXIdentifier(a.name) && a.name.name === "component"
4297
+ );
4298
+ let componentFile = null;
4299
+ if (t8.isJSXAttribute(componentAttr) && t8.isJSXExpressionContainer(componentAttr.value)) {
4300
+ const expr = componentAttr.value.expression;
4301
+ if (t8.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
4302
+ }
4303
+ if (componentFile) this.routeTargets.set(screenName, componentFile);
4304
+ return {
4305
+ name: screenName,
4306
+ navigatorName,
4307
+ navigatorType,
4308
+ ...componentFile ? { componentFile } : {}
4309
+ };
4310
+ }
2881
4311
  /** Extract string attribute value from JSX attributes */
2882
4312
  extractAttributeValue(attributes, attrName) {
2883
4313
  const attr = attributes.find(
2884
- (a) => t5.isJSXAttribute(a) && t5.isJSXIdentifier(a.name) && a.name.name === attrName
4314
+ (a) => t8.isJSXAttribute(a) && t8.isJSXIdentifier(a.name) && a.name.name === attrName
2885
4315
  );
2886
- if (t5.isJSXAttribute(attr) && t5.isStringLiteral(attr.value)) {
4316
+ if (t8.isJSXAttribute(attr) && t8.isStringLiteral(attr.value)) {
2887
4317
  return attr.value.value;
2888
4318
  }
2889
4319
  return null;
@@ -2892,7 +4322,7 @@ var NavigationAnalyzer = class {
2892
4322
  parseParamTypes(content, filePath) {
2893
4323
  const types = [];
2894
4324
  try {
2895
- const ast = parser.parse(content, {
4325
+ const ast = parser2.parse(content, {
2896
4326
  sourceType: "module",
2897
4327
  plugins: [
2898
4328
  "jsx",
@@ -2900,7 +4330,7 @@ var NavigationAnalyzer = class {
2900
4330
  ...this.config.parserPlugins || []
2901
4331
  ]
2902
4332
  });
2903
- (0, import_traverse5.default)(ast, {
4333
+ (0, import_traverse6.default)(ast, {
2904
4334
  TSTypeAliasDeclaration: (nodePath) => {
2905
4335
  const { node } = nodePath;
2906
4336
  const typeName = node.id.name;
@@ -2973,7 +4403,7 @@ var NavigationAnalyzer = class {
2973
4403
  if (type.type === "TSUndefinedKeyword") return "undefined";
2974
4404
  if (type.type === "TSNullKeyword") return "null";
2975
4405
  if (type.type === "TSUnionType") {
2976
- return type.types.map((t15) => this.typeToString(t15)).join(" | ");
4406
+ return type.types.map((t18) => this.typeToString(t18)).join(" | ");
2977
4407
  }
2978
4408
  if (type.type === "TSTypeLiteral") {
2979
4409
  return "object";
@@ -2994,7 +4424,7 @@ var NavigationAnalyzer = class {
2994
4424
  /** Attach parsed type params to navigator screens */
2995
4425
  attachParamsToNavigators(navigators, types) {
2996
4426
  for (const navigator of navigators) {
2997
- const matchingType = types.find((t15) => t15.type === navigator.type);
4427
+ const matchingType = types.find((t18) => t18.type === navigator.type);
2998
4428
  if (matchingType) {
2999
4429
  for (const screen of navigator.screens) {
3000
4430
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -3072,9 +4502,9 @@ var NavigationAnalyzer = class {
3072
4502
 
3073
4503
  // src/analyzers/ComponentAnalyzer.ts
3074
4504
  var import_fs5 = require("fs");
3075
- var parser2 = __toESM(require("@babel/parser"));
3076
- var import_traverse6 = __toESM(require("@babel/traverse"));
3077
- var t6 = __toESM(require("@babel/types"));
4505
+ var parser3 = __toESM(require("@babel/parser"));
4506
+ var import_traverse7 = __toESM(require("@babel/traverse"));
4507
+ var t9 = __toESM(require("@babel/types"));
3078
4508
  var ComponentAnalyzer = class {
3079
4509
  config;
3080
4510
  constructor(config) {
@@ -3084,14 +4514,14 @@ var ComponentAnalyzer = class {
3084
4514
  async analyzeFile(filePath) {
3085
4515
  try {
3086
4516
  const code = (0, import_fs5.readFileSync)(filePath, "utf-8");
3087
- const ast = parser2.parse(code, {
4517
+ const ast = parser3.parse(code, {
3088
4518
  sourceType: "module",
3089
4519
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
3090
4520
  });
3091
4521
  const components = [];
3092
- (0, import_traverse6.default)(ast, {
3093
- JSXElement: (path11) => {
3094
- const component = this.extractComponentFromJSXElement(path11.node);
4522
+ (0, import_traverse7.default)(ast, {
4523
+ JSXElement: (path13) => {
4524
+ const component = this.extractComponentFromJSXElement(path13.node);
3095
4525
  if (component) {
3096
4526
  components.push(component);
3097
4527
  }
@@ -3120,19 +4550,19 @@ var ComponentAnalyzer = class {
3120
4550
  };
3121
4551
  }
3122
4552
  getElementName(openingElement) {
3123
- if (t6.isJSXIdentifier(openingElement.name)) {
4553
+ if (t9.isJSXIdentifier(openingElement.name)) {
3124
4554
  return openingElement.name.name;
3125
4555
  }
3126
- if (t6.isJSXMemberExpression(openingElement.name)) {
4556
+ if (t9.isJSXMemberExpression(openingElement.name)) {
3127
4557
  const parts = [];
3128
4558
  let current = openingElement.name;
3129
- while (t6.isJSXMemberExpression(current)) {
3130
- if (t6.isJSXIdentifier(current.property)) {
4559
+ while (t9.isJSXMemberExpression(current)) {
4560
+ if (t9.isJSXIdentifier(current.property)) {
3131
4561
  parts.unshift(current.property.name);
3132
4562
  }
3133
4563
  current = current.object;
3134
4564
  }
3135
- if (t6.isJSXIdentifier(current)) {
4565
+ if (t9.isJSXIdentifier(current)) {
3136
4566
  parts.unshift(current.name);
3137
4567
  }
3138
4568
  return parts.join(".");
@@ -3155,13 +4585,13 @@ var ComponentAnalyzer = class {
3155
4585
  extractProps(openingElement) {
3156
4586
  const props = {};
3157
4587
  for (const attr of openingElement.attributes) {
3158
- if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
4588
+ if (t9.isJSXAttribute(attr) && t9.isJSXIdentifier(attr.name)) {
3159
4589
  const propName = attr.name.name;
3160
4590
  if (attr.value === null) {
3161
4591
  props[propName] = "true";
3162
- } else if (t6.isStringLiteral(attr.value)) {
4592
+ } else if (t9.isStringLiteral(attr.value)) {
3163
4593
  props[propName] = attr.value.value;
3164
- } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
4594
+ } else if (t9.isJSXExpressionContainer(attr.value) && t9.isStringLiteral(attr.value.expression)) {
3165
4595
  props[propName] = attr.value.expression.value;
3166
4596
  }
3167
4597
  }
@@ -3171,12 +4601,12 @@ var ComponentAnalyzer = class {
3171
4601
  extractAccessibilityProps(openingElement) {
3172
4602
  const result = {};
3173
4603
  for (const attr of openingElement.attributes) {
3174
- if (t6.isJSXAttribute(attr) && t6.isJSXIdentifier(attr.name)) {
4604
+ if (t9.isJSXAttribute(attr) && t9.isJSXIdentifier(attr.name)) {
3175
4605
  const propName = attr.name.name;
3176
4606
  if ((propName === "testID" || propName === "accessibilityLabel") && attr.value) {
3177
- if (t6.isStringLiteral(attr.value)) {
4607
+ if (t9.isStringLiteral(attr.value)) {
3178
4608
  result[propName] = attr.value.value;
3179
- } else if (t6.isJSXExpressionContainer(attr.value) && t6.isStringLiteral(attr.value.expression)) {
4609
+ } else if (t9.isJSXExpressionContainer(attr.value) && t9.isStringLiteral(attr.value.expression)) {
3180
4610
  result[propName] = attr.value.expression.value;
3181
4611
  }
3182
4612
  }
@@ -3188,7 +4618,7 @@ var ComponentAnalyzer = class {
3188
4618
  if (depth >= maxDepth) return [];
3189
4619
  const extracted = [];
3190
4620
  for (const child of children) {
3191
- if (t6.isJSXElement(child)) {
4621
+ if (t9.isJSXElement(child)) {
3192
4622
  const component = this.extractComponentFromJSXElement(child);
3193
4623
  if (component) {
3194
4624
  extracted.push(component);
@@ -3201,10 +4631,10 @@ var ComponentAnalyzer = class {
3201
4631
 
3202
4632
  // src/analyzers/FormAnalyzer.ts
3203
4633
  var import_fs6 = require("fs");
3204
- var parser3 = __toESM(require("@babel/parser"));
3205
- var import_traverse7 = __toESM(require("@babel/traverse"));
3206
- var t7 = __toESM(require("@babel/types"));
3207
- var path3 = __toESM(require("path"));
4634
+ var parser4 = __toESM(require("@babel/parser"));
4635
+ var import_traverse8 = __toESM(require("@babel/traverse"));
4636
+ var t10 = __toESM(require("@babel/types"));
4637
+ var path5 = __toESM(require("path"));
3208
4638
  var FormAnalyzer = class {
3209
4639
  config;
3210
4640
  stateVariables = /* @__PURE__ */ new Map();
@@ -3217,21 +4647,21 @@ var FormAnalyzer = class {
3217
4647
  async analyzeFile(filePath) {
3218
4648
  try {
3219
4649
  const code = (0, import_fs6.readFileSync)(filePath, "utf-8");
3220
- const ast = parser3.parse(code, {
4650
+ const ast = parser4.parse(code, {
3221
4651
  sourceType: "module",
3222
4652
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
3223
4653
  });
3224
4654
  this.stateVariables.clear();
3225
4655
  this.inputElements = [];
3226
4656
  this.submitButtons = [];
3227
- (0, import_traverse7.default)(ast, {
3228
- CallExpression: (path11) => {
3229
- this.extractStateVariables(path11.node);
4657
+ (0, import_traverse8.default)(ast, {
4658
+ CallExpression: (path13) => {
4659
+ this.extractStateVariables(path13.node);
3230
4660
  }
3231
4661
  });
3232
- (0, import_traverse7.default)(ast, {
3233
- JSXElement: (path11) => {
3234
- this.extractFormElements(path11.node);
4662
+ (0, import_traverse8.default)(ast, {
4663
+ JSXElement: (path13) => {
4664
+ this.extractFormElements(path13.node);
3235
4665
  }
3236
4666
  });
3237
4667
  const validationRules = this.extractValidationRules(ast);
@@ -3242,7 +4672,7 @@ var FormAnalyzer = class {
3242
4672
  }
3243
4673
  }
3244
4674
  extractStateVariables(node) {
3245
- if (t7.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
4675
+ if (t10.isIdentifier(node.callee) && node.callee.name === "useState" && node.arguments.length > 0) {
3246
4676
  return;
3247
4677
  }
3248
4678
  }
@@ -3263,28 +4693,28 @@ var FormAnalyzer = class {
3263
4693
  extractInputInfo(openingElement) {
3264
4694
  const info2 = { varName: "" };
3265
4695
  for (const attr of openingElement.attributes) {
3266
- if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
4696
+ if (t10.isJSXAttribute(attr) && t10.isJSXIdentifier(attr.name)) {
3267
4697
  const propName = attr.name.name;
3268
- const propValue = this.extractAttributeValue(attr.value);
4698
+ const propValue2 = this.extractAttributeValue(attr.value);
3269
4699
  switch (propName) {
3270
4700
  case "label":
3271
- info2.label = propValue;
4701
+ info2.label = propValue2;
3272
4702
  break;
3273
4703
  case "placeholder":
3274
- info2.placeholder = propValue;
4704
+ info2.placeholder = propValue2;
3275
4705
  break;
3276
4706
  case "keyboardType":
3277
- info2.keyboardType = propValue;
4707
+ info2.keyboardType = propValue2;
3278
4708
  break;
3279
4709
  case "testID":
3280
- info2.testID = propValue;
4710
+ info2.testID = propValue2;
3281
4711
  break;
3282
4712
  case "appilotsId":
3283
- info2.appilotsId = propValue;
3284
- if (propValue) info2.varName = propValue;
4713
+ info2.appilotsId = propValue2;
4714
+ if (propValue2) info2.varName = propValue2;
3285
4715
  break;
3286
4716
  case "value":
3287
- if (attr.value && t7.isJSXExpressionContainer(attr.value) && t7.isIdentifier(attr.value.expression)) {
4717
+ if (attr.value && t10.isJSXExpressionContainer(attr.value) && t10.isIdentifier(attr.value.expression)) {
3288
4718
  info2.varName = attr.value.expression.name;
3289
4719
  }
3290
4720
  break;
@@ -3296,16 +4726,16 @@ var FormAnalyzer = class {
3296
4726
  extractButtonInfo(openingElement) {
3297
4727
  const info2 = {};
3298
4728
  for (const attr of openingElement.attributes) {
3299
- if (t7.isJSXAttribute(attr) && t7.isJSXIdentifier(attr.name)) {
4729
+ if (t10.isJSXAttribute(attr) && t10.isJSXIdentifier(attr.name)) {
3300
4730
  const propName = attr.name.name;
3301
4731
  if (propName === "title" || propName === "label") {
3302
4732
  info2.label = this.extractAttributeValue(attr.value);
3303
4733
  }
3304
4734
  if (propName === "onPress") {
3305
- if (attr.value && t7.isJSXExpressionContainer(attr.value)) {
3306
- if (t7.isIdentifier(attr.value.expression)) {
4735
+ if (attr.value && t10.isJSXExpressionContainer(attr.value)) {
4736
+ if (t10.isIdentifier(attr.value.expression)) {
3307
4737
  info2.handler = attr.value.expression.name;
3308
- } else if (t7.isArrowFunctionExpression(attr.value.expression) || t7.isFunctionExpression(attr.value.expression)) {
4738
+ } else if (t10.isArrowFunctionExpression(attr.value.expression) || t10.isFunctionExpression(attr.value.expression)) {
3309
4739
  info2.handler = "anonymous";
3310
4740
  }
3311
4741
  }
@@ -3316,23 +4746,23 @@ var FormAnalyzer = class {
3316
4746
  }
3317
4747
  extractAttributeValue(value) {
3318
4748
  if (value === null) return void 0;
3319
- if (t7.isStringLiteral(value)) return value.value;
3320
- if (t7.isJSXExpressionContainer(value) && t7.isStringLiteral(value.expression)) {
4749
+ if (t10.isStringLiteral(value)) return value.value;
4750
+ if (t10.isJSXExpressionContainer(value) && t10.isStringLiteral(value.expression)) {
3321
4751
  return value.expression.value;
3322
4752
  }
3323
4753
  return void 0;
3324
4754
  }
3325
4755
  getElementName(openingElement) {
3326
- if (t7.isJSXIdentifier(openingElement.name)) {
4756
+ if (t10.isJSXIdentifier(openingElement.name)) {
3327
4757
  return openingElement.name.name;
3328
4758
  }
3329
4759
  return null;
3330
4760
  }
3331
4761
  extractValidationRules(ast) {
3332
4762
  const rules = {};
3333
- (0, import_traverse7.default)(ast, {
3334
- IfStatement: (path11) => {
3335
- const test = path11.node.test;
4763
+ (0, import_traverse8.default)(ast, {
4764
+ IfStatement: (path13) => {
4765
+ const test = path13.node.test;
3336
4766
  const rule = this.extractRuleFromCondition(test);
3337
4767
  if (rule) {
3338
4768
  const { field, description } = rule;
@@ -3347,36 +4777,36 @@ var FormAnalyzer = class {
3347
4777
  extractRuleFromCondition(test) {
3348
4778
  let fieldName = "";
3349
4779
  let description = "";
3350
- if (t7.isUnaryExpression(test) && test.operator === "!") {
3351
- if (t7.isCallExpression(test.argument)) {
4780
+ if (t10.isUnaryExpression(test) && test.operator === "!") {
4781
+ if (t10.isCallExpression(test.argument)) {
3352
4782
  const callExpr = test.argument;
3353
- if (t7.isMemberExpression(callExpr.callee)) {
4783
+ if (t10.isMemberExpression(callExpr.callee)) {
3354
4784
  const memberExpr = callExpr.callee;
3355
- if (t7.isIdentifier(memberExpr.object)) {
4785
+ if (t10.isIdentifier(memberExpr.object)) {
3356
4786
  fieldName = memberExpr.object.name;
3357
4787
  description = `${fieldName} is required`;
3358
4788
  }
3359
4789
  }
3360
- } else if (t7.isIdentifier(test.argument)) {
4790
+ } else if (t10.isIdentifier(test.argument)) {
3361
4791
  fieldName = test.argument.name;
3362
4792
  description = `${fieldName} is required`;
3363
4793
  }
3364
4794
  }
3365
- if (t7.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
3366
- if (t7.isMemberExpression(test.left)) {
4795
+ if (t10.isBinaryExpression(test) && (test.operator === "<" || test.operator === "<=")) {
4796
+ if (t10.isMemberExpression(test.left)) {
3367
4797
  const memberExpr = test.left;
3368
- if (t7.isIdentifier(memberExpr.object)) {
4798
+ if (t10.isIdentifier(memberExpr.object)) {
3369
4799
  fieldName = memberExpr.object.name;
3370
- if (t7.isNumericLiteral(test.right)) {
4800
+ if (t10.isNumericLiteral(test.right)) {
3371
4801
  description = `${fieldName} must be at least ${test.right.value} characters`;
3372
4802
  }
3373
4803
  }
3374
4804
  }
3375
4805
  }
3376
- if (t7.isUnaryExpression(test) && test.operator === "!") {
3377
- if (t7.isCallExpression(test.argument) && t7.isMemberExpression(test.argument.callee)) {
3378
- const methodName = t7.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
3379
- if (methodName === "includes" && t7.isIdentifier(test.argument.callee.object)) {
4806
+ if (t10.isUnaryExpression(test) && test.operator === "!") {
4807
+ if (t10.isCallExpression(test.argument) && t10.isMemberExpression(test.argument.callee)) {
4808
+ const methodName = t10.isIdentifier(test.argument.callee.property) ? test.argument.callee.property.name : null;
4809
+ if (methodName === "includes" && t10.isIdentifier(test.argument.callee.object)) {
3380
4810
  fieldName = test.argument.callee.object.name;
3381
4811
  description = `${fieldName} format is invalid`;
3382
4812
  }
@@ -3386,7 +4816,7 @@ var FormAnalyzer = class {
3386
4816
  }
3387
4817
  buildForms(filePath, validationRules) {
3388
4818
  if (this.inputElements.length === 0) return [];
3389
- const fileName = path3.basename(filePath, path3.extname(filePath));
4819
+ const fileName = path5.basename(filePath, path5.extname(filePath));
3390
4820
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
3391
4821
  const fields = this.inputElements.map((input) => {
3392
4822
  const fieldType = this.inferFieldType(input);
@@ -3436,14 +4866,104 @@ var FormAnalyzer = class {
3436
4866
  }
3437
4867
  };
3438
4868
 
4869
+ // src/pipeline/composition.ts
4870
+ var MAX_DEPTH = 3;
4871
+ var MAX_VISITED_PER_SCREEN = 60;
4872
+ var MAX_FAN_IN = 3;
4873
+ var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
4874
+ function descendants(graph, from, screenFiles) {
4875
+ const seen = /* @__PURE__ */ new Set([from]);
4876
+ const out = [];
4877
+ let frontier = [from];
4878
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
4879
+ const next = [];
4880
+ for (const file2 of frontier) {
4881
+ for (const child of graph.renderedComponentFiles(file2)) {
4882
+ if (seen.has(child)) continue;
4883
+ seen.add(child);
4884
+ if (screenFiles.has(child)) continue;
4885
+ out.push(child);
4886
+ next.push(child);
4887
+ if (out.length >= MAX_VISITED_PER_SCREEN) return out;
4888
+ }
4889
+ }
4890
+ frontier = next;
4891
+ }
4892
+ return out;
4893
+ }
4894
+ function addressable(locator) {
4895
+ if (!locator || typeof locator !== "object") return false;
4896
+ const l = locator;
4897
+ if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
4898
+ return typeof l["testID"] === "string" && l["testID"].length > 0;
4899
+ }
4900
+ async function composeAffordances(options) {
4901
+ const { screens, graph, analyzeFile, screenFiles } = options;
4902
+ const byScreen = /* @__PURE__ */ new Map();
4903
+ const fanIn = /* @__PURE__ */ new Map();
4904
+ for (const screen of screens) {
4905
+ if (!screen.filePath) continue;
4906
+ const children = descendants(graph, screen.filePath, screenFiles);
4907
+ byScreen.set(screen, children);
4908
+ for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
4909
+ }
4910
+ const analyzed = /* @__PURE__ */ new Map();
4911
+ let screensEnriched = 0;
4912
+ let filesMerged = 0;
4913
+ for (const [screen, children] of byScreen) {
4914
+ const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
4915
+ if (exclusive.length === 0) continue;
4916
+ const actionIds = new Set(screen.actions.map((a) => a.id));
4917
+ const targetIds = new Set((screen.targets ?? []).map((t18) => t18.id));
4918
+ const formIds = new Set(screen.forms.map((f) => f.id));
4919
+ let gained = false;
4920
+ for (const file2 of exclusive) {
4921
+ if (!analyzed.has(file2)) {
4922
+ try {
4923
+ analyzed.set(file2, await analyzeFile(file2));
4924
+ } catch {
4925
+ analyzed.set(file2, null);
4926
+ }
4927
+ }
4928
+ const child = analyzed.get(file2);
4929
+ if (!child) continue;
4930
+ let merged = false;
4931
+ for (const action of child.actions) {
4932
+ if (!addressable(action.locator)) continue;
4933
+ if (actionIds.has(action.id)) continue;
4934
+ actionIds.add(action.id);
4935
+ screen.actions.push(action);
4936
+ merged = true;
4937
+ }
4938
+ for (const target of child.targets ?? []) {
4939
+ if (!addressable(target.locator)) continue;
4940
+ if (targetIds.has(target.id)) continue;
4941
+ targetIds.add(target.id);
4942
+ (screen.targets ??= []).push(target);
4943
+ merged = true;
4944
+ }
4945
+ for (const form of child.forms) {
4946
+ if (form.fields.length === 0) continue;
4947
+ const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
4948
+ if (formIds.has(id)) continue;
4949
+ formIds.add(id);
4950
+ screen.forms.push({ ...form, id });
4951
+ merged = true;
4952
+ }
4953
+ if (merged) {
4954
+ filesMerged++;
4955
+ gained = true;
4956
+ }
4957
+ }
4958
+ if (gained) screensEnriched++;
4959
+ }
4960
+ return { screens, screensEnriched, filesMerged };
4961
+ }
4962
+
3439
4963
  // src/analyzers/ReactNativePlatformAnalyzer.ts
3440
4964
  var ReactNativePlatformAnalyzer = class {
3441
4965
  platform = "react-native";
3442
4966
  async analyze(config, options) {
3443
- const screenAnalyzer = new ScreenAnalyzer(config, {
3444
- strictScreens: options.strictScreens ?? true,
3445
- screenPatterns: options.screenPatterns
3446
- });
3447
4967
  const navigationAnalyzer = new NavigationAnalyzer(config, {
3448
4968
  navigationInclude: options.navigationInclude,
3449
4969
  navigationExclude: options.navigationExclude
@@ -3451,14 +4971,31 @@ var ReactNativePlatformAnalyzer = class {
3451
4971
  const componentAnalyzer = new ComponentAnalyzer(config);
3452
4972
  const formAnalyzer = new FormAnalyzer(config);
3453
4973
  console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
3454
- const [screens, navigation] = await Promise.all([
3455
- screenAnalyzer.analyze(),
3456
- navigationAnalyzer.analyze()
3457
- ]);
4974
+ const navigation = await navigationAnalyzer.analyze();
4975
+ const screenAnalyzer = new ScreenAnalyzer(config, {
4976
+ strictScreens: options.strictScreens ?? true,
4977
+ screenPatterns: options.screenPatterns,
4978
+ // Menos os arquivos que declaram navegador: ver `navigatorFiles`.
4979
+ routeTargetFiles: new Set(
4980
+ [...navigationAnalyzer.routeTargets.values()].filter(
4981
+ (file2) => !navigationAnalyzer.navigatorFiles.has(file2)
4982
+ )
4983
+ )
4984
+ });
4985
+ const screens = await screenAnalyzer.analyze();
4986
+ const composed = await composeAffordances({
4987
+ screens,
4988
+ graph: navigationAnalyzer.graph,
4989
+ analyzeFile: (file2) => screenAnalyzer.analyzeFile(file2),
4990
+ screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
4991
+ });
4992
+ console.log(
4993
+ `[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
4994
+ );
3458
4995
  console.log(
3459
4996
  `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
3460
4997
  );
3461
- const screenFiles = await (0, import_fast_glob3.default)(config.include || ["**/*.tsx", "**/*.ts"], {
4998
+ const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
3462
4999
  cwd: config.rootDir,
3463
5000
  ignore: config.exclude || ["**/node_modules/**"]
3464
5001
  });
@@ -3466,7 +5003,7 @@ var ReactNativePlatformAnalyzer = class {
3466
5003
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
3467
5004
  );
3468
5005
  const enrichmentPromises = screenFiles.map(async (file2) => {
3469
- const filePath = import_node_path.default.resolve(config.rootDir, file2);
5006
+ const filePath = import_node_path3.default.resolve(config.rootDir, file2);
3470
5007
  try {
3471
5008
  const [components, forms] = await Promise.all([
3472
5009
  componentAnalyzer.analyzeFile(filePath),
@@ -3519,9 +5056,11 @@ var ReactNativePlatformAnalyzer = class {
3519
5056
  });
3520
5057
  return {
3521
5058
  screens: enrichedScreens,
5059
+ controlEvidenceFiles: screenAnalyzer.controlEvidenceFiles,
3522
5060
  navigation,
3523
5061
  analyzedFiles: screenFiles.length,
3524
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
5062
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
5063
+ diagnostics: { navigation: navigationAnalyzer.diagnostics }
3525
5064
  };
3526
5065
  }
3527
5066
  mergeForm(target, source) {
@@ -3539,9 +5078,12 @@ var ReactNativePlatformAnalyzer = class {
3539
5078
  findEquivalentField(fields, incoming) {
3540
5079
  return fields.find((field) => {
3541
5080
  if (field.name && incoming.name && field.name === incoming.name) return true;
3542
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
3543
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
3544
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
5081
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
5082
+ return true;
5083
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
5084
+ return true;
5085
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
5086
+ return true;
3545
5087
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
3546
5088
  return true;
3547
5089
  }
@@ -3574,7 +5116,9 @@ var ReactNativePlatformAnalyzer = class {
3574
5116
  if (incoming.fields.length === 0) return void 0;
3575
5117
  let best;
3576
5118
  for (const form of forms) {
3577
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
5119
+ const overlap = incoming.fields.filter(
5120
+ (field) => this.findEquivalentField(form.fields, field)
5121
+ ).length;
3578
5122
  if (overlap > 0 && (!best || overlap > best.overlap)) {
3579
5123
  best = { form, overlap };
3580
5124
  }
@@ -3601,9 +5145,8 @@ var GenericPlatformAnalyzer = class {
3601
5145
  // src/analyzers/web/WebScreenAnalyzer.ts
3602
5146
  var import_promises2 = __toESM(require("fs/promises"));
3603
5147
  var import_path5 = __toESM(require("path"));
3604
- var import_fast_glob4 = __toESM(require("fast-glob"));
3605
- var import_traverse9 = __toESM(require("@babel/traverse"));
3606
- var t10 = __toESM(require("@babel/types"));
5148
+ var import_traverse10 = __toESM(require("@babel/traverse"));
5149
+ var t13 = __toESM(require("@babel/types"));
3607
5150
 
3608
5151
  // src/ast/jsx/web/classify.ts
3609
5152
  var VIEW_TAGS = /* @__PURE__ */ new Set([
@@ -3719,49 +5262,49 @@ function classifyWebJsxComponent(name, element) {
3719
5262
  }
3720
5263
 
3721
5264
  // src/ast/jsx/names.ts
3722
- var t8 = __toESM(require("@babel/types"));
5265
+ var t11 = __toESM(require("@babel/types"));
3723
5266
  function getJsxElementName(openingElement) {
3724
5267
  return getJsxName(openingElement.name);
3725
5268
  }
3726
5269
  function getJsxName(name) {
3727
- if (t8.isJSXIdentifier(name)) return name.name;
3728
- if (t8.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
3729
- if (t8.isJSXMemberExpression(name)) {
5270
+ if (t11.isJSXIdentifier(name)) return name.name;
5271
+ if (t11.isJSXNamespacedName(name)) return `${name.namespace.name}:${name.name.name}`;
5272
+ if (t11.isJSXMemberExpression(name)) {
3730
5273
  const objectName = getJsxName(name.object);
3731
- const propertyName = t8.isJSXIdentifier(name.property) ? name.property.name : null;
5274
+ const propertyName = t11.isJSXIdentifier(name.property) ? name.property.name : null;
3732
5275
  return objectName && propertyName ? `${objectName}.${propertyName}` : null;
3733
5276
  }
3734
5277
  return null;
3735
5278
  }
3736
5279
 
3737
5280
  // src/ast/navigation/web-calls.ts
3738
- var import_traverse8 = __toESM(require("@babel/traverse"));
3739
- var t9 = __toESM(require("@babel/types"));
5281
+ var import_traverse9 = __toESM(require("@babel/traverse"));
5282
+ var t12 = __toESM(require("@babel/types"));
3740
5283
  var ROUTERISH_OBJECTS = /^(router|history|navigation)$/;
3741
5284
  function staticRoutePath(node) {
3742
5285
  if (!node) return void 0;
3743
- if (t9.isStringLiteral(node)) return node.value;
3744
- if (t9.isTemplateLiteral(node)) {
3745
- let path11 = "";
5286
+ if (t12.isStringLiteral(node)) return node.value;
5287
+ if (t12.isTemplateLiteral(node)) {
5288
+ let path13 = "";
3746
5289
  node.quasis.forEach((quasi, index) => {
3747
- path11 += quasi.value.cooked ?? quasi.value.raw;
5290
+ path13 += quasi.value.cooked ?? quasi.value.raw;
3748
5291
  const expr = node.expressions[index];
3749
- if (expr) path11 += `:${paramNameOf(expr)}`;
5292
+ if (expr) path13 += `:${paramNameOf(expr)}`;
3750
5293
  });
3751
- return path11;
5294
+ return path13;
3752
5295
  }
3753
5296
  return void 0;
3754
5297
  }
3755
5298
  function paramNameOf(expr) {
3756
- if (t9.isIdentifier(expr)) return expr.name;
3757
- if (t9.isMemberExpression(expr) && t9.isIdentifier(expr.property)) return expr.property.name;
5299
+ if (t12.isIdentifier(expr)) return expr.name;
5300
+ if (t12.isMemberExpression(expr) && t12.isIdentifier(expr.property)) return expr.property.name;
3758
5301
  return "param";
3759
5302
  }
3760
5303
  function extractWebNavigationCalls(ast) {
3761
5304
  const calls = [];
3762
5305
  const inspect = (node) => {
3763
- if (t9.isCallExpression(node)) {
3764
- if (t9.isIdentifier(node.callee) && node.callee.name === "navigate") {
5306
+ if (t12.isCallExpression(node)) {
5307
+ if (t12.isIdentifier(node.callee) && node.callee.name === "navigate") {
3765
5308
  const first = node.arguments[0];
3766
5309
  const targetPath = staticRoutePath(first);
3767
5310
  if (targetPath !== void 0) {
@@ -3769,12 +5312,12 @@ function extractWebNavigationCalls(ast) {
3769
5312
  method: hasReplaceOption(node.arguments[1]) ? "replace" : "navigate",
3770
5313
  targetPath
3771
5314
  });
3772
- } else if (t9.isNumericLiteral(first) || t9.isUnaryExpression(first) && first.operator === "-") {
5315
+ } else if (t12.isNumericLiteral(first) || t12.isUnaryExpression(first) && first.operator === "-") {
3773
5316
  calls.push({ method: "goBack" });
3774
5317
  }
3775
5318
  return;
3776
5319
  }
3777
- if (t9.isMemberExpression(node.callee) && t9.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t9.isIdentifier(node.callee.property)) {
5320
+ if (t12.isMemberExpression(node.callee) && t12.isIdentifier(node.callee.object) && ROUTERISH_OBJECTS.test(node.callee.object.name) && t12.isIdentifier(node.callee.property)) {
3778
5321
  const method = node.callee.property.name;
3779
5322
  const targetPath = staticRoutePath(node.arguments[0]);
3780
5323
  if ((method === "push" || method === "navigate") && targetPath !== void 0) {
@@ -3787,25 +5330,25 @@ function extractWebNavigationCalls(ast) {
3787
5330
  }
3788
5331
  return;
3789
5332
  }
3790
- if (t9.isAssignmentExpression(node) && t9.isMemberExpression(node.left) && t9.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && t9.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
5333
+ if (t12.isAssignmentExpression(node) && t12.isMemberExpression(node.left) && t12.isIdentifier(node.left.property) && node.left.property.name === "href" && isLocationExpression(node.left.object) && t12.isStringLiteral(node.right) && node.right.value.startsWith("/")) {
3791
5334
  calls.push({ method: "navigate", targetPath: node.right.value });
3792
5335
  }
3793
5336
  };
3794
- (0, import_traverse8.default)(ast, {
3795
- noScope: !t9.isFile(ast),
5337
+ (0, import_traverse9.default)(ast, {
5338
+ noScope: !t12.isFile(ast),
3796
5339
  enter: (nodePath) => inspect(nodePath.node)
3797
5340
  });
3798
5341
  return calls;
3799
5342
  }
3800
5343
  function hasReplaceOption(arg) {
3801
- if (!arg || !t9.isObjectExpression(arg)) return false;
5344
+ if (!arg || !t12.isObjectExpression(arg)) return false;
3802
5345
  return arg.properties.some(
3803
- (prop) => t9.isObjectProperty(prop) && t9.isIdentifier(prop.key) && prop.key.name === "replace" && t9.isBooleanLiteral(prop.value) && prop.value.value === true
5346
+ (prop) => t12.isObjectProperty(prop) && t12.isIdentifier(prop.key) && prop.key.name === "replace" && t12.isBooleanLiteral(prop.value) && prop.value.value === true
3804
5347
  );
3805
5348
  }
3806
5349
  function isLocationExpression(node) {
3807
- if (t9.isIdentifier(node)) return node.name === "location";
3808
- return t9.isMemberExpression(node) && t9.isIdentifier(node.object) && node.object.name === "window" && t9.isIdentifier(node.property) && node.property.name === "location";
5350
+ if (t12.isIdentifier(node)) return node.name === "location";
5351
+ return t12.isMemberExpression(node) && t12.isIdentifier(node.object) && node.object.name === "window" && t12.isIdentifier(node.property) && node.property.name === "location";
3809
5352
  }
3810
5353
 
3811
5354
  // src/analyzers/web/WebScreenAnalyzer.ts
@@ -3833,10 +5376,10 @@ var WebScreenAnalyzer = class {
3833
5376
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
3834
5377
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
3835
5378
  } = this.config;
3836
- const files = (await (0, import_fast_glob4.default)(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
5379
+ const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
3837
5380
  (file2) => !file2.endsWith(".d.ts")
3838
5381
  );
3839
- const patternMatches = await (0, import_fast_glob4.default)(this.screenPatterns, {
5382
+ const patternMatches = await globSorted(this.screenPatterns, {
3840
5383
  cwd: this.config.rootDir,
3841
5384
  ignore: exclude
3842
5385
  });
@@ -3913,7 +5456,7 @@ var WebScreenAnalyzer = class {
3913
5456
  // ── registerScreen ────────────────────────────────────────────────
3914
5457
  detectRegisterScreenCall(ast) {
3915
5458
  let found = false;
3916
- (0, import_traverse9.default)(ast, {
5459
+ (0, import_traverse10.default)(ast, {
3917
5460
  CallExpression: (nodePath) => {
3918
5461
  if (found) return;
3919
5462
  if (isRegisterScreenCallee(nodePath.node.callee)) {
@@ -3926,11 +5469,11 @@ var WebScreenAnalyzer = class {
3926
5469
  }
3927
5470
  extractRegisterScreenMetadata(ast) {
3928
5471
  let plain = null;
3929
- (0, import_traverse9.default)(ast, {
5472
+ (0, import_traverse10.default)(ast, {
3930
5473
  CallExpression: (nodePath) => {
3931
5474
  if (!isRegisterScreenCallee(nodePath.node.callee)) return;
3932
5475
  const arg = nodePath.node.arguments[0];
3933
- if (t10.isObjectExpression(arg)) {
5476
+ if (t13.isObjectExpression(arg)) {
3934
5477
  plain = literalToPlain(arg);
3935
5478
  }
3936
5479
  }
@@ -3974,23 +5517,23 @@ var WebScreenAnalyzer = class {
3974
5517
  extractComponentName(ast) {
3975
5518
  let defaultName = "";
3976
5519
  let firstExported = "";
3977
- (0, import_traverse9.default)(ast, {
5520
+ (0, import_traverse10.default)(ast, {
3978
5521
  ExportDefaultDeclaration: (nodePath) => {
3979
5522
  const declaration = nodePath.node.declaration;
3980
- if (t10.isFunctionDeclaration(declaration) && declaration.id?.name) {
5523
+ if (t13.isFunctionDeclaration(declaration) && declaration.id?.name) {
3981
5524
  defaultName = declaration.id.name;
3982
- } else if (t10.isIdentifier(declaration)) {
5525
+ } else if (t13.isIdentifier(declaration)) {
3983
5526
  defaultName = declaration.name;
3984
5527
  }
3985
5528
  },
3986
5529
  ExportNamedDeclaration: (nodePath) => {
3987
5530
  if (firstExported) return;
3988
5531
  const declaration = nodePath.node.declaration;
3989
- if (t10.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
5532
+ if (t13.isFunctionDeclaration(declaration) && declaration.id && /^[A-Z]/.test(declaration.id.name)) {
3990
5533
  firstExported = declaration.id.name;
3991
- } else if (t10.isVariableDeclaration(declaration)) {
5534
+ } else if (t13.isVariableDeclaration(declaration)) {
3992
5535
  for (const declarator of declaration.declarations) {
3993
- if (t10.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t10.isArrowFunctionExpression(declarator.init) || t10.isFunctionExpression(declarator.init))) {
5536
+ if (t13.isIdentifier(declarator.id) && /^[A-Z]/.test(declarator.id.name) && (t13.isArrowFunctionExpression(declarator.init) || t13.isFunctionExpression(declarator.init))) {
3994
5537
  firstExported = declarator.id.name;
3995
5538
  break;
3996
5539
  }
@@ -4003,7 +5546,7 @@ var WebScreenAnalyzer = class {
4003
5546
  extractComponents(ast) {
4004
5547
  const components = [];
4005
5548
  const seen = /* @__PURE__ */ new Set();
4006
- (0, import_traverse9.default)(ast, {
5549
+ (0, import_traverse10.default)(ast, {
4007
5550
  JSXOpeningElement: (nodePath) => {
4008
5551
  const element = nodePath.node;
4009
5552
  const name = getJsxElementName(element);
@@ -4024,7 +5567,7 @@ var WebScreenAnalyzer = class {
4024
5567
  // ── <label htmlFor> association ───────────────────────────────────
4025
5568
  collectHtmlForLabels(ast) {
4026
5569
  const labels = /* @__PURE__ */ new Map();
4027
- (0, import_traverse9.default)(ast, {
5570
+ (0, import_traverse10.default)(ast, {
4028
5571
  JSXElement: (nodePath) => {
4029
5572
  const element = nodePath.node;
4030
5573
  if (getJsxElementName(element.openingElement) !== "label") return;
@@ -4067,7 +5610,7 @@ var WebScreenAnalyzer = class {
4067
5610
  formBuckets.set(formElement, bucket);
4068
5611
  return bucket;
4069
5612
  };
4070
- (0, import_traverse9.default)(ast, {
5613
+ (0, import_traverse10.default)(ast, {
4071
5614
  JSXElement: (nodePath) => {
4072
5615
  const element = nodePath.node;
4073
5616
  const name = getJsxElementName(element.openingElement);
@@ -4084,7 +5627,7 @@ var WebScreenAnalyzer = class {
4084
5627
  if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
4085
5628
  }
4086
5629
  });
4087
- (0, import_traverse9.default)(ast, {
5630
+ (0, import_traverse10.default)(ast, {
4088
5631
  JSXElement: (nodePath) => {
4089
5632
  const element = nodePath.node;
4090
5633
  const name = getJsxElementName(element.openingElement);
@@ -4162,15 +5705,15 @@ var WebScreenAnalyzer = class {
4162
5705
  });
4163
5706
  }
4164
5707
  for (const attr of opening.attributes) {
4165
- if (!t10.isJSXAttribute(attr) || !t10.isJSXIdentifier(attr.name)) continue;
5708
+ if (!t13.isJSXAttribute(attr) || !t13.isJSXIdentifier(attr.name)) continue;
4166
5709
  const attrName = attr.name.name;
4167
5710
  if (attrName === "required") {
4168
5711
  if (attr.value === null) field.required = true;
4169
- else if (t10.isJSXExpressionContainer(attr.value) && t10.isBooleanLiteral(attr.value.expression)) {
5712
+ else if (t13.isJSXExpressionContainer(attr.value) && t13.isBooleanLiteral(attr.value.expression)) {
4170
5713
  field.required = attr.value.expression.value;
4171
5714
  }
4172
5715
  }
4173
- if ((attrName === "value" || attrName === "checked") && attr.value && t10.isJSXExpressionContainer(attr.value) && t10.isIdentifier(attr.value.expression)) {
5716
+ if ((attrName === "value" || attrName === "checked") && attr.value && t13.isJSXExpressionContainer(attr.value) && t13.isIdentifier(attr.value.expression)) {
4174
5717
  field.valueBinding = attr.value.expression.name;
4175
5718
  if (!field.name) field.name = attr.value.expression.name;
4176
5719
  }
@@ -4220,7 +5763,7 @@ var WebScreenAnalyzer = class {
4220
5763
  extractSelectOptions(selectElement) {
4221
5764
  const options = [];
4222
5765
  for (const child of selectElement.children) {
4223
- if (!t10.isJSXElement(child)) continue;
5766
+ if (!t13.isJSXElement(child)) continue;
4224
5767
  if (getJsxElementName(child.openingElement) !== "option") continue;
4225
5768
  const value = getStringAttr(child.openingElement, "value");
4226
5769
  const label = jsxTextContent(child) || value || "";
@@ -4262,7 +5805,7 @@ var WebScreenAnalyzer = class {
4262
5805
  const actionLabels = new Map(
4263
5806
  actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
4264
5807
  );
4265
- (0, import_traverse9.default)(ast, {
5808
+ (0, import_traverse10.default)(ast, {
4266
5809
  JSXElement: (nodePath) => {
4267
5810
  const element = nodePath.node;
4268
5811
  const name = getJsxElementName(element.openingElement);
@@ -4331,14 +5874,14 @@ var WebScreenAnalyzer = class {
4331
5874
  }
4332
5875
  let handlerName;
4333
5876
  const onClickAttr = opening.attributes.find(
4334
- (attr) => t10.isJSXAttribute(attr) && t10.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
5877
+ (attr) => t13.isJSXAttribute(attr) && t13.isJSXIdentifier(attr.name) && attr.name.name === "onClick"
4335
5878
  );
4336
- if (onClickAttr?.value && t10.isJSXExpressionContainer(onClickAttr.value)) {
5879
+ if (onClickAttr?.value && t13.isJSXExpressionContainer(onClickAttr.value)) {
4337
5880
  const expr = onClickAttr.value.expression;
4338
- if (t10.isIdentifier(expr)) {
5881
+ if (t13.isIdentifier(expr)) {
4339
5882
  handlerName = expr.name;
4340
5883
  action.handler = expr.name;
4341
- } else if (!t10.isJSXEmptyExpression(expr)) {
5884
+ } else if (!t13.isJSXEmptyExpression(expr)) {
4342
5885
  const inlineNavCalls = extractWebNavigationCalls(expr);
4343
5886
  const inlineNav = inlineNavCalls.find((call) => call.targetPath);
4344
5887
  if (inlineNav?.targetPath) {
@@ -4477,12 +6020,12 @@ var WebScreenAnalyzer = class {
4477
6020
  }
4478
6021
  handlerNameFromAttr(opening, attrName) {
4479
6022
  const attr = opening.attributes.find(
4480
- (candidate) => t10.isJSXAttribute(candidate) && t10.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
6023
+ (candidate) => t13.isJSXAttribute(candidate) && t13.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
4481
6024
  );
4482
- if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
6025
+ if (!attr?.value || !t13.isJSXExpressionContainer(attr.value)) return void 0;
4483
6026
  const expr = attr.value.expression;
4484
- if (t10.isIdentifier(expr)) return expr.name;
4485
- if (t10.isArrowFunctionExpression(expr) || t10.isFunctionExpression(expr)) {
6027
+ if (t13.isIdentifier(expr)) return expr.name;
6028
+ if (t13.isArrowFunctionExpression(expr) || t13.isFunctionExpression(expr)) {
4486
6029
  return firstCalledFunctionName(expr) ?? "anonymous";
4487
6030
  }
4488
6031
  return void 0;
@@ -4493,7 +6036,7 @@ var WebScreenAnalyzer = class {
4493
6036
  for (const call of extractWebNavigationCalls(ast)) {
4494
6037
  if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
4495
6038
  }
4496
- (0, import_traverse9.default)(ast, {
6039
+ (0, import_traverse10.default)(ast, {
4497
6040
  JSXOpeningElement: (nodePath) => {
4498
6041
  const element = nodePath.node;
4499
6042
  const name = getJsxElementName(element);
@@ -4513,14 +6056,14 @@ var WebScreenAnalyzer = class {
4513
6056
  extractCollections(ast) {
4514
6057
  const collections = [];
4515
6058
  const seen = /* @__PURE__ */ new Set();
4516
- (0, import_traverse9.default)(ast, {
6059
+ (0, import_traverse10.default)(ast, {
4517
6060
  JSXExpressionContainer: (nodePath) => {
4518
6061
  const expr = nodePath.node.expression;
4519
- if (!t10.isCallExpression(expr) || !t10.isMemberExpression(expr.callee) || !t10.isIdentifier(expr.callee.object) || !t10.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
6062
+ if (!t13.isCallExpression(expr) || !t13.isMemberExpression(expr.callee) || !t13.isIdentifier(expr.callee.object) || !t13.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
4520
6063
  return;
4521
6064
  }
4522
6065
  const callback = expr.arguments[0];
4523
- if (!t10.isArrowFunctionExpression(callback) && !t10.isFunctionExpression(callback)) return;
6066
+ if (!t13.isArrowFunctionExpression(callback) && !t13.isFunctionExpression(callback)) return;
4524
6067
  const enclosing = nodePath.findParent(
4525
6068
  (p) => p.isJSXElement()
4526
6069
  );
@@ -4555,22 +6098,22 @@ var WebScreenAnalyzer = class {
4555
6098
  }
4556
6099
  };
4557
6100
  function isRegisterScreenCallee(callee) {
4558
- return t10.isIdentifier(callee) && callee.name === "registerScreen" || t10.isMemberExpression(callee) && t10.isIdentifier(callee.property) && callee.property.name === "registerScreen";
6101
+ return t13.isIdentifier(callee) && callee.name === "registerScreen" || t13.isMemberExpression(callee) && t13.isIdentifier(callee.property) && callee.property.name === "registerScreen";
4559
6102
  }
4560
6103
  function literalToPlain(node) {
4561
- if (t10.isStringLiteral(node) || t10.isNumericLiteral(node) || t10.isBooleanLiteral(node)) {
6104
+ if (t13.isStringLiteral(node) || t13.isNumericLiteral(node) || t13.isBooleanLiteral(node)) {
4562
6105
  return node.value;
4563
6106
  }
4564
- if (t10.isNullLiteral(node)) return null;
4565
- if (t10.isArrayExpression(node)) {
4566
- return node.elements.filter((el) => el !== null && t10.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
6107
+ if (t13.isNullLiteral(node)) return null;
6108
+ if (t13.isArrayExpression(node)) {
6109
+ return node.elements.filter((el) => el !== null && t13.isExpression(el)).map((el) => literalToPlain(el)).filter((value) => value !== void 0);
4567
6110
  }
4568
- if (t10.isObjectExpression(node)) {
6111
+ if (t13.isObjectExpression(node)) {
4569
6112
  const out = {};
4570
6113
  for (const prop of node.properties) {
4571
- if (!t10.isObjectProperty(prop)) continue;
4572
- const key = t10.isIdentifier(prop.key) ? prop.key.name : t10.isStringLiteral(prop.key) ? prop.key.value : void 0;
4573
- if (!key || !t10.isExpression(prop.value)) continue;
6114
+ if (!t13.isObjectProperty(prop)) continue;
6115
+ const key = t13.isIdentifier(prop.key) ? prop.key.name : t13.isStringLiteral(prop.key) ? prop.key.value : void 0;
6116
+ if (!key || !t13.isExpression(prop.value)) continue;
4574
6117
  const value = literalToPlain(prop.value);
4575
6118
  if (value !== void 0) out[key] = value;
4576
6119
  }
@@ -4582,12 +6125,12 @@ function jsxTextContent(element) {
4582
6125
  const parts = [];
4583
6126
  const walk = (children) => {
4584
6127
  for (const child of children) {
4585
- if (t10.isJSXText(child)) {
6128
+ if (t13.isJSXText(child)) {
4586
6129
  const trimmed = child.value.replace(/\s+/g, " ").trim();
4587
6130
  if (trimmed) parts.push(trimmed);
4588
- } else if (t10.isJSXExpressionContainer(child) && t10.isStringLiteral(child.expression)) {
6131
+ } else if (t13.isJSXExpressionContainer(child) && t13.isStringLiteral(child.expression)) {
4589
6132
  parts.push(child.expression.value);
4590
- } else if (t10.isJSXElement(child)) {
6133
+ } else if (t13.isJSXElement(child)) {
4591
6134
  walk(child.children);
4592
6135
  }
4593
6136
  }
@@ -4597,26 +6140,26 @@ function jsxTextContent(element) {
4597
6140
  return text.length > 0 ? text : void 0;
4598
6141
  }
4599
6142
  function firstCalledFunctionName(node) {
4600
- if (t10.isIdentifier(node)) return node.name;
4601
- if (t10.isArrowFunctionExpression(node) || t10.isFunctionExpression(node)) {
6143
+ if (t13.isIdentifier(node)) return node.name;
6144
+ if (t13.isArrowFunctionExpression(node) || t13.isFunctionExpression(node)) {
4602
6145
  return firstCalledFunctionName(node.body);
4603
6146
  }
4604
- if (t10.isBlockStatement(node)) {
6147
+ if (t13.isBlockStatement(node)) {
4605
6148
  for (const statement of node.body) {
4606
6149
  const handler = firstCalledFunctionName(statement);
4607
6150
  if (handler) return handler;
4608
6151
  }
4609
6152
  return void 0;
4610
6153
  }
4611
- if (t10.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
4612
- if (t10.isReturnStatement(node)) {
6154
+ if (t13.isExpressionStatement(node)) return firstCalledFunctionName(node.expression);
6155
+ if (t13.isReturnStatement(node)) {
4613
6156
  return node.argument ? firstCalledFunctionName(node.argument) : void 0;
4614
6157
  }
4615
- if (t10.isAwaitExpression(node) || t10.isUnaryExpression(node)) {
6158
+ if (t13.isAwaitExpression(node) || t13.isUnaryExpression(node)) {
4616
6159
  return firstCalledFunctionName(node.argument);
4617
6160
  }
4618
- if (t10.isCallExpression(node)) {
4619
- if (t10.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
6161
+ if (t13.isCallExpression(node)) {
6162
+ if (t13.isIdentifier(node.callee) && !/^(navigate|confirm|alert)$/.test(node.callee.name)) {
4620
6163
  return node.callee.name;
4621
6164
  }
4622
6165
  return void 0;
@@ -4625,14 +6168,14 @@ function firstCalledFunctionName(node) {
4625
6168
  }
4626
6169
  function containsWindowConfirm(body) {
4627
6170
  let found = false;
4628
- (0, import_traverse9.default)(
6171
+ (0, import_traverse10.default)(
4629
6172
  body,
4630
6173
  {
4631
6174
  noScope: true,
4632
6175
  CallExpression: (nodePath) => {
4633
6176
  const callee = nodePath.node.callee;
4634
- if (t10.isIdentifier(callee) && callee.name === "confirm") found = true;
4635
- if (t10.isMemberExpression(callee) && t10.isIdentifier(callee.object) && callee.object.name === "window" && t10.isIdentifier(callee.property) && callee.property.name === "confirm") {
6177
+ if (t13.isIdentifier(callee) && callee.name === "confirm") found = true;
6178
+ if (t13.isMemberExpression(callee) && t13.isIdentifier(callee.object) && callee.object.name === "window" && t13.isIdentifier(callee.property) && callee.property.name === "confirm") {
4636
6179
  found = true;
4637
6180
  }
4638
6181
  }
@@ -4647,9 +6190,9 @@ function routePathAttr(opening, attrName) {
4647
6190
  const literal = getStringAttr(opening, attrName);
4648
6191
  if (literal) return literal;
4649
6192
  const attr = opening.attributes.find(
4650
- (candidate) => t10.isJSXAttribute(candidate) && t10.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
6193
+ (candidate) => t13.isJSXAttribute(candidate) && t13.isJSXIdentifier(candidate.name) && candidate.name.name === attrName
4651
6194
  );
4652
- if (!attr?.value || !t10.isJSXExpressionContainer(attr.value)) return void 0;
6195
+ if (!attr?.value || !t13.isJSXExpressionContainer(attr.value)) return void 0;
4653
6196
  return staticRoutePath(attr.value.expression);
4654
6197
  }
4655
6198
  function slugify(label) {
@@ -4664,10 +6207,10 @@ function isWeakInferredFieldName(name) {
4664
6207
  function collectionItemNames(callback) {
4665
6208
  const names = /* @__PURE__ */ new Set(["item"]);
4666
6209
  const firstParam = callback.params[0];
4667
- if (t10.isIdentifier(firstParam)) names.add(firstParam.name);
4668
- if (t10.isObjectPattern(firstParam)) {
6210
+ if (t13.isIdentifier(firstParam)) names.add(firstParam.name);
6211
+ if (t13.isObjectPattern(firstParam)) {
4669
6212
  for (const prop of firstParam.properties) {
4670
- if (t10.isObjectProperty(prop) && t10.isIdentifier(prop.key) && t10.isIdentifier(prop.value)) {
6213
+ if (t13.isObjectProperty(prop) && t13.isIdentifier(prop.key) && t13.isIdentifier(prop.value)) {
4671
6214
  names.add(prop.value.name);
4672
6215
  }
4673
6216
  }
@@ -4677,13 +6220,13 @@ function collectionItemNames(callback) {
4677
6220
  function collectionDisplayFields(callback, itemNames) {
4678
6221
  const fields = /* @__PURE__ */ new Set();
4679
6222
  if (!callback.body) return [];
4680
- (0, import_traverse9.default)(
6223
+ (0, import_traverse10.default)(
4681
6224
  callback.body,
4682
6225
  {
4683
6226
  noScope: true,
4684
6227
  MemberExpression: (nodePath) => {
4685
6228
  const node = nodePath.node;
4686
- if (t10.isIdentifier(node.object) && itemNames.has(node.object.name) && t10.isIdentifier(node.property)) {
6229
+ if (t13.isIdentifier(node.object) && itemNames.has(node.object.name) && t13.isIdentifier(node.property)) {
4687
6230
  fields.add(node.property.name);
4688
6231
  }
4689
6232
  }
@@ -4694,16 +6237,16 @@ function collectionDisplayFields(callback, itemNames) {
4694
6237
  function collectionKeyField(callback, itemNames) {
4695
6238
  let keyField;
4696
6239
  if (!callback.body) return void 0;
4697
- (0, import_traverse9.default)(
6240
+ (0, import_traverse10.default)(
4698
6241
  callback.body,
4699
6242
  {
4700
6243
  noScope: true,
4701
6244
  JSXAttribute: (nodePath) => {
4702
6245
  const attr = nodePath.node;
4703
- if (!t10.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
4704
- if (!attr.value || !t10.isJSXExpressionContainer(attr.value)) return;
6246
+ if (!t13.isJSXIdentifier(attr.name) || attr.name.name !== "key") return;
6247
+ if (!attr.value || !t13.isJSXExpressionContainer(attr.value)) return;
4705
6248
  const expr = attr.value.expression;
4706
- if (t10.isMemberExpression(expr) && t10.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t10.isIdentifier(expr.property)) {
6249
+ if (t13.isMemberExpression(expr) && t13.isIdentifier(expr.object) && itemNames.has(expr.object.name) && t13.isIdentifier(expr.property)) {
4707
6250
  keyField = keyField ?? expr.property.name;
4708
6251
  }
4709
6252
  }
@@ -4726,18 +6269,18 @@ function callbackReturnsTag(callback, tags) {
4726
6269
  let found = false;
4727
6270
  const inspect = (node) => {
4728
6271
  if (!node || found) return;
4729
- if (t10.isJSXElement(node)) {
6272
+ if (t13.isJSXElement(node)) {
4730
6273
  const name = getJsxElementName(node.openingElement);
4731
6274
  if (name && tags.has(name)) found = true;
4732
6275
  return;
4733
6276
  }
4734
- if (t10.isBlockStatement(node)) {
6277
+ if (t13.isBlockStatement(node)) {
4735
6278
  for (const statement of node.body) {
4736
- if (t10.isReturnStatement(statement)) inspect(statement.argument);
6279
+ if (t13.isReturnStatement(statement)) inspect(statement.argument);
4737
6280
  }
4738
6281
  }
4739
- if (t10.isParenthesizedExpression(node)) inspect(node.expression);
4740
- if (t10.isConditionalExpression(node)) {
6282
+ if (t13.isParenthesizedExpression(node)) inspect(node.expression);
6283
+ if (t13.isConditionalExpression(node)) {
4741
6284
  inspect(node.consequent);
4742
6285
  inspect(node.alternate);
4743
6286
  }
@@ -4764,9 +6307,8 @@ function inferIdentityFields(keyField, displayFields) {
4764
6307
  // src/analyzers/web/WebNavigationAnalyzer.ts
4765
6308
  var import_fs7 = require("fs");
4766
6309
  var import_path6 = __toESM(require("path"));
4767
- var import_fast_glob5 = __toESM(require("fast-glob"));
4768
- var import_traverse10 = __toESM(require("@babel/traverse"));
4769
- var t11 = __toESM(require("@babel/types"));
6310
+ var import_traverse11 = __toESM(require("@babel/traverse"));
6311
+ var t14 = __toESM(require("@babel/types"));
4770
6312
  var WEB_NAVIGATOR_TYPE = "route";
4771
6313
  var WebNavigationAnalyzer = class {
4772
6314
  config;
@@ -4783,7 +6325,9 @@ var WebNavigationAnalyzer = class {
4783
6325
  for (const filePath of files) {
4784
6326
  try {
4785
6327
  const content = await import_fs7.promises.readFile(filePath, "utf-8");
4786
- if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
6328
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
6329
+ content
6330
+ )) {
4787
6331
  continue;
4788
6332
  }
4789
6333
  const ast = parseSource(content, this.config.parserPlugins);
@@ -4813,7 +6357,7 @@ var WebNavigationAnalyzer = class {
4813
6357
  ...this.config.exclude || [],
4814
6358
  ...this.navigationExclude
4815
6359
  ];
4816
- const files = await (0, import_fast_glob5.default)(patterns, { cwd: this.config.rootDir, ignore });
6360
+ const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
4817
6361
  return files.map((file2) => import_path6.default.join(this.config.rootDir, file2));
4818
6362
  }
4819
6363
  // ── JSX <Route> style ────────────────────────────────────────────
@@ -4824,7 +6368,7 @@ var WebNavigationAnalyzer = class {
4824
6368
  const name = getJsxElementName(opening);
4825
6369
  if (name !== "Route") {
4826
6370
  for (const child of element.children) {
4827
- if (t11.isJSXElement(child)) visitRoute(child, parentPath);
6371
+ if (t14.isJSXElement(child)) visitRoute(child, parentPath);
4828
6372
  }
4829
6373
  return;
4830
6374
  }
@@ -4833,16 +6377,16 @@ var WebNavigationAnalyzer = class {
4833
6377
  const fullPath = this.joinPaths(parentPath, segment, isIndex);
4834
6378
  const componentName = this.componentNameFromElementAttr(opening) ?? void 0;
4835
6379
  const isLeaf = !element.children.some(
4836
- (child) => t11.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
6380
+ (child) => t14.isJSXElement(child) && getJsxElementName(child.openingElement) === "Route"
4837
6381
  );
4838
6382
  if ((segment || isIndex) && (componentName || isLeaf)) {
4839
6383
  routes.push(this.buildRoute(fullPath, componentName, isIndex, !isLeaf));
4840
6384
  }
4841
6385
  for (const child of element.children) {
4842
- if (t11.isJSXElement(child)) visitRoute(child, fullPath);
6386
+ if (t14.isJSXElement(child)) visitRoute(child, fullPath);
4843
6387
  }
4844
6388
  };
4845
- (0, import_traverse10.default)(ast, {
6389
+ (0, import_traverse11.default)(ast, {
4846
6390
  JSXElement: (nodePath) => {
4847
6391
  const name = getJsxElementName(nodePath.node.openingElement);
4848
6392
  if (name !== "Routes" && name !== "Route") return;
@@ -4861,13 +6405,13 @@ var WebNavigationAnalyzer = class {
4861
6405
  /** `element={<VehicleList/>}` or `Component={VehicleList}`. */
4862
6406
  componentNameFromElementAttr(opening) {
4863
6407
  for (const attr of opening.attributes) {
4864
- if (!t11.isJSXAttribute(attr) || !t11.isJSXIdentifier(attr.name)) continue;
4865
- if (attr.name.name === "element" && t11.isJSXExpressionContainer(attr.value)) {
6408
+ if (!t14.isJSXAttribute(attr) || !t14.isJSXIdentifier(attr.name)) continue;
6409
+ if (attr.name.name === "element" && t14.isJSXExpressionContainer(attr.value)) {
4866
6410
  const expr = attr.value.expression;
4867
- if (t11.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
6411
+ if (t14.isJSXElement(expr)) return getJsxElementName(expr.openingElement);
4868
6412
  }
4869
- if (attr.name.name === "Component" && t11.isJSXExpressionContainer(attr.value)) {
4870
- if (t11.isIdentifier(attr.value.expression)) return attr.value.expression.name;
6413
+ if (attr.name.name === "Component" && t14.isJSXExpressionContainer(attr.value)) {
6414
+ if (t14.isIdentifier(attr.value.expression)) return attr.value.expression.name;
4871
6415
  }
4872
6416
  }
4873
6417
  return null;
@@ -4881,12 +6425,12 @@ var WebNavigationAnalyzer = class {
4881
6425
  "createMemoryRouter",
4882
6426
  "useRoutes"
4883
6427
  ]);
4884
- (0, import_traverse10.default)(ast, {
6428
+ (0, import_traverse11.default)(ast, {
4885
6429
  CallExpression: (nodePath) => {
4886
6430
  const callee = nodePath.node.callee;
4887
- if (!t11.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
6431
+ if (!t14.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
4888
6432
  const first = nodePath.node.arguments[0];
4889
- if (!t11.isArrayExpression(first)) return;
6433
+ if (!t14.isArrayExpression(first)) return;
4890
6434
  this.visitRouteObjects(first, "", routes);
4891
6435
  }
4892
6436
  });
@@ -4894,21 +6438,21 @@ var WebNavigationAnalyzer = class {
4894
6438
  }
4895
6439
  visitRouteObjects(arr, parentPath, out) {
4896
6440
  for (const element of arr.elements) {
4897
- if (!t11.isObjectExpression(element)) continue;
6441
+ if (!t14.isObjectExpression(element)) continue;
4898
6442
  let segment;
4899
6443
  let isIndex = false;
4900
6444
  let componentName;
4901
6445
  let children;
4902
6446
  for (const prop of element.properties) {
4903
- if (!t11.isObjectProperty(prop) || !t11.isIdentifier(prop.key)) continue;
6447
+ if (!t14.isObjectProperty(prop) || !t14.isIdentifier(prop.key)) continue;
4904
6448
  const key = prop.key.name;
4905
- if (key === "path" && t11.isStringLiteral(prop.value)) segment = prop.value.value;
4906
- if (key === "index" && t11.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
4907
- if (key === "element" && t11.isJSXElement(prop.value)) {
6449
+ if (key === "path" && t14.isStringLiteral(prop.value)) segment = prop.value.value;
6450
+ if (key === "index" && t14.isBooleanLiteral(prop.value)) isIndex = prop.value.value;
6451
+ if (key === "element" && t14.isJSXElement(prop.value)) {
4908
6452
  componentName = getJsxElementName(prop.value.openingElement) ?? void 0;
4909
6453
  }
4910
- if (key === "Component" && t11.isIdentifier(prop.value)) componentName = prop.value.name;
4911
- if (key === "children" && t11.isArrayExpression(prop.value)) children = prop.value;
6454
+ if (key === "Component" && t14.isIdentifier(prop.value)) componentName = prop.value.name;
6455
+ if (key === "children" && t14.isArrayExpression(prop.value)) children = prop.value;
4912
6456
  }
4913
6457
  const fullPath = this.joinPaths(parentPath, segment, isIndex);
4914
6458
  if ((segment !== void 0 || isIndex) && (componentName || !children)) {
@@ -4986,11 +6530,13 @@ var WebNavigationAnalyzer = class {
4986
6530
  return {
4987
6531
  screens,
4988
6532
  initialScreen: initialRoute?.screenName ?? "",
4989
- navigators: routes.length > 0 ? [{
4990
- name: navigatorName,
4991
- type: WEB_NAVIGATOR_TYPE,
4992
- screens: screenNames
4993
- }] : []
6533
+ navigators: routes.length > 0 ? [
6534
+ {
6535
+ name: navigatorName,
6536
+ type: WEB_NAVIGATOR_TYPE,
6537
+ screens: screenNames
6538
+ }
6539
+ ] : []
4994
6540
  };
4995
6541
  }
4996
6542
  };
@@ -5067,23 +6613,23 @@ var ReactWebPlatformAnalyzer = class {
5067
6613
  }
5068
6614
  /** Replace route-path references with screen names where the route table resolves them. */
5069
6615
  resolveRoutePaths(screen, routes) {
5070
- const resolve2 = (target) => {
6616
+ const resolve3 = (target) => {
5071
6617
  if (!target || !target.startsWith("/")) return target;
5072
6618
  return resolvePathToScreen(routes, target) ?? target;
5073
6619
  };
5074
6620
  const navigationTargets = Array.from(
5075
6621
  new Set(
5076
- screen.navigationTargets.map((target) => resolve2(target)).filter((target) => target !== screen.name)
6622
+ screen.navigationTargets.map((target) => resolve3(target)).filter((target) => target !== screen.name)
5077
6623
  )
5078
6624
  ).sort();
5079
6625
  const actions = screen.actions.map((action) => {
5080
- const resolved = resolve2(action.targetScreen);
6626
+ const resolved = resolve3(action.targetScreen);
5081
6627
  return resolved === action.targetScreen ? action : { ...action, targetScreen: resolved };
5082
6628
  });
5083
6629
  const collections = screen.collections?.map((collection) => {
5084
6630
  const resolveRow = (row) => {
5085
6631
  if (!row?.targetScreen) return row;
5086
- const resolved = resolve2(row.targetScreen);
6632
+ const resolved = resolve3(row.targetScreen);
5087
6633
  if (resolved === row.targetScreen) return row;
5088
6634
  return {
5089
6635
  ...row,
@@ -5109,7 +6655,7 @@ var ReactWebPlatformAnalyzer = class {
5109
6655
 
5110
6656
  // src/manifest/loadManifest.ts
5111
6657
  var import_promises3 = require("fs/promises");
5112
- var import_node_path2 = __toESM(require("path"));
6658
+ var import_node_path4 = __toESM(require("path"));
5113
6659
 
5114
6660
  // ../../node_modules/zod/v3/external.js
5115
6661
  var external_exports = {};
@@ -5316,8 +6862,8 @@ var ZodParsedType = util.arrayToEnum([
5316
6862
  "set"
5317
6863
  ]);
5318
6864
  var getParsedType = (data) => {
5319
- const t15 = typeof data;
5320
- switch (t15) {
6865
+ const t18 = typeof data;
6866
+ switch (t18) {
5321
6867
  case "undefined":
5322
6868
  return ZodParsedType.undefined;
5323
6869
  case "string":
@@ -5589,8 +7135,8 @@ function getErrorMap() {
5589
7135
 
5590
7136
  // ../../node_modules/zod/v3/helpers/parseUtil.js
5591
7137
  var makeIssue = (params) => {
5592
- const { data, path: path11, errorMaps, issueData } = params;
5593
- const fullPath = [...path11, ...issueData.path || []];
7138
+ const { data, path: path13, errorMaps, issueData } = params;
7139
+ const fullPath = [...path13, ...issueData.path || []];
5594
7140
  const fullIssue = {
5595
7141
  ...issueData,
5596
7142
  path: fullPath
@@ -5706,11 +7252,11 @@ var errorUtil;
5706
7252
 
5707
7253
  // ../../node_modules/zod/v3/types.js
5708
7254
  var ParseInputLazyPath = class {
5709
- constructor(parent, value, path11, key) {
7255
+ constructor(parent, value, path13, key) {
5710
7256
  this._cachedPath = [];
5711
7257
  this.parent = parent;
5712
7258
  this.data = value;
5713
- this._path = path11;
7259
+ this._path = path13;
5714
7260
  this._key = key;
5715
7261
  }
5716
7262
  get path() {
@@ -9152,7 +10698,36 @@ var coerce = {
9152
10698
  };
9153
10699
  var NEVER = INVALID;
9154
10700
 
9155
- // ../shared/dist/chunk-MKT45LRA.mjs
10701
+ // ../shared/dist/chunk-SWQOZWHP.mjs
10702
+ var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
10703
+ var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
10704
+ function isSafeImageUrl(value) {
10705
+ if (value === null || value === void 0) return true;
10706
+ const trimmed = value.trim();
10707
+ if (trimmed === "") return true;
10708
+ if (!HAS_SCHEME_RE.test(trimmed)) return true;
10709
+ return SAFE_IMAGE_URL_RE.test(trimmed);
10710
+ }
10711
+
10712
+ // ../shared/dist/chunk-KUMPS6RH.mjs
10713
+ var SUPPORTED_APPILOTS_LOCALES = ["pt-BR", "en", "es", "fr"];
10714
+
10715
+ // ../shared/dist/chunk-2GTRKNPJ.mjs
10716
+ var symbols = external_exports.array(external_exports.string().max(240)).max(12);
10717
+ var controlEvidenceSchema = external_exports.object({
10718
+ version: external_exports.literal(1),
10719
+ siteId: external_exports.string().regex(/^[a-f0-9]{20}$/),
10720
+ component: external_exports.string().max(240),
10721
+ icons: symbols,
10722
+ handler: external_exports.string().max(240).optional(),
10723
+ calls: symbols,
10724
+ argumentBindings: symbols,
10725
+ conditions: symbols,
10726
+ nativeConfirmation: external_exports.object({
10727
+ title: external_exports.string().max(240).optional(),
10728
+ destructiveOption: external_exports.boolean()
10729
+ }).optional()
10730
+ });
9156
10731
  var locatorSourceSchema = external_exports.enum([
9157
10732
  "appilotsId",
9158
10733
  "testID",
@@ -9331,6 +10906,23 @@ var actionDescriptorSchema = external_exports.object({
9331
10906
  effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
9332
10907
  riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
9333
10908
  requiresConfirmation: external_exports.boolean().optional(),
10909
+ /**
10910
+ * How long this action's work is expected to take, in milliseconds,
10911
+ * DECLARED by the app — not inferred (that is `appilotsInferred`).
10912
+ *
10913
+ * The SDK's post-action wait was a constant: 6s, or 10s when the
10914
+ * generator could prove the handler awaits something. No constant
10915
+ * fits, because app operations run from ~100ms to minutes, and the
10916
+ * failure is silent in both directions — too short and the agent
10917
+ * photographs a loading screen with no controls on it, too long and
10918
+ * every fast press pays for the slowest one. Only the app knows.
10919
+ *
10920
+ * Absent means absent: the SDK keeps its current defaults, so a
10921
+ * document generated before this field existed behaves exactly as it
10922
+ * did. The SDK also clamps the value to its own safety ceiling — a
10923
+ * declared budget is a request, not a licence to hang the session.
10924
+ */
10925
+ asyncBudgetMs: external_exports.number().int().positive().optional(),
9334
10926
  appilotsInferred: appilotsInferredActionSchema.optional()
9335
10927
  }).passthrough();
9336
10928
  var screenPermissionDescriptorSchema = external_exports.object({
@@ -9513,7 +11105,7 @@ var uploadMCPDocumentSchema = external_exports.object({
9513
11105
  version: external_exports.string().default("1.0"),
9514
11106
  content: external_exports.record(external_exports.unknown())
9515
11107
  });
9516
- var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
11108
+ var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
9517
11109
  var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
9518
11110
  var createApiKeySchema = external_exports.object({
9519
11111
  name: external_exports.string().min(1).max(100),
@@ -9524,8 +11116,8 @@ var createApiKeySchema = external_exports.object({
9524
11116
  scope: apiKeyScopeSchema.default("sdk"),
9525
11117
  environment: apiKeyEnvironmentSchema.optional(),
9526
11118
  expiresAt: external_exports.string().datetime().optional()
9527
- }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
9528
- message: 'projectId is required for scope "sdk"',
11119
+ }).refine((v) => v.scope === "operator" || !!v.projectId, {
11120
+ message: "projectId is required for SDK and publishing keys",
9529
11121
  path: ["projectId"]
9530
11122
  });
9531
11123
  var boundedString = (max) => external_exports.string().max(max);
@@ -9550,10 +11142,21 @@ var snapshotInputSchema = external_exports.object({
9550
11142
  type: boundedString(40).optional(),
9551
11143
  required: external_exports.boolean().optional(),
9552
11144
  invalid: external_exports.boolean().optional(),
11145
+ /**
11146
+ * Whether the field holds anything, as its own fact rather than an
11147
+ * inference over `value`. The value is the user's and is the first
11148
+ * thing a privacy policy withholds; the existence of a value is the
11149
+ * agent's and is what stops it filling the same field twice.
11150
+ */
11151
+ filled: external_exports.boolean().optional(),
11152
+ /** This input has keyboard focus right now. */
11153
+ focused: external_exports.boolean().optional(),
9553
11154
  inModal: external_exports.boolean().optional()
9554
11155
  }).passthrough();
9555
11156
  var snapshotButtonSchema = external_exports.object({
11157
+ controlEvidence: controlEvidenceSchema.optional(),
9556
11158
  id: boundedString(160).optional(),
11159
+ dispatchable: external_exports.boolean().optional(),
9557
11160
  provenance: identityProvenanceSchema.optional(),
9558
11161
  /**
9559
11162
  * False when the control is mounted but currently OUTSIDE the window —
@@ -9614,7 +11217,36 @@ var snapshotListSchema = external_exports.object({
9614
11217
  */
9615
11218
  source: boundedString(40).optional(),
9616
11219
  itemCount: external_exports.number().int().optional(),
11220
+ /**
11221
+ * Size of the whole collection when the app declared it, for a list
11222
+ * that is a WINDOW onto more data than it holds.
11223
+ *
11224
+ * `itemCount` is how many rows the list is rendering from and
11225
+ * `visibleItemCount` how many of those are mounted; neither can
11226
+ * express "there are 36 and you are looking at the first 20",
11227
+ * because a paginated list's `data` is the page. Absent means
11228
+ * unknown — never "same as itemCount".
11229
+ */
11230
+ totalItemCount: external_exports.number().int().optional(),
9617
11231
  visibleItemCount: external_exports.number().int().optional(),
11232
+ viewportItemCount: external_exports.number().int().nonnegative().optional(),
11233
+ exploration: external_exports.object({
11234
+ revision: external_exports.number().int().nonnegative(),
11235
+ observedItemCount: external_exports.number().int().min(0).max(5e3),
11236
+ observedRanges: external_exports.array(
11237
+ external_exports.object({
11238
+ start: external_exports.number().int().nonnegative(),
11239
+ end: external_exports.number().int().nonnegative()
11240
+ })
11241
+ ).max(32),
11242
+ rangesTruncated: external_exports.boolean(),
11243
+ coverage: external_exports.enum(["partial", "all-loaded"]),
11244
+ pagination: external_exports.enum(["possible", "not-declared"]),
11245
+ scrollSteps: external_exports.number().int().nonnegative(),
11246
+ remainingScrollSteps: external_exports.number().int().nonnegative(),
11247
+ consecutiveNoProgress: external_exports.number().int().nonnegative(),
11248
+ lastScroll: external_exports.enum(["moved", "no-progress", "boundary", "unverified"]).optional()
11249
+ }).optional(),
9618
11250
  refreshing: external_exports.boolean().optional(),
9619
11251
  empty: external_exports.boolean().optional(),
9620
11252
  label: boundedString(300).optional(),
@@ -9662,6 +11294,7 @@ var snapshotChoiceGroupSchema = external_exports.object({
9662
11294
  }).passthrough();
9663
11295
  var snapshotElementSchema = external_exports.object({
9664
11296
  id: boundedString(160).optional(),
11297
+ dispatchable: external_exports.boolean().optional(),
9665
11298
  role: boundedString(40).optional(),
9666
11299
  label: boundedString(300).optional(),
9667
11300
  texts: external_exports.array(boundedString(500)).max(50).optional(),
@@ -9705,6 +11338,34 @@ var agentSnapshotSchema = external_exports.object({
9705
11338
  * it. Optional: older SDKs never clamp and never send it.
9706
11339
  */
9707
11340
  truncated: external_exports.boolean().optional(),
11341
+ /**
11342
+ * What moved since the previous observation, computed on the device
11343
+ * because that is the only side holding both snapshots.
11344
+ *
11345
+ * Shape only — counts, booleans, and ids the app declared itself — so
11346
+ * it survives an observation whose content was withheld. ABSENT means
11347
+ * there was no previous observation to compare against; `unchanged:
11348
+ * true` means we compared and nothing moved, which is the strongest
11349
+ * evidence available that an action did nothing. The two must not be
11350
+ * collapsed, for the same reason `truncated` exists.
11351
+ */
11352
+ delta: external_exports.object({
11353
+ routeChanged: external_exports.boolean().optional(),
11354
+ modalOpened: external_exports.boolean().optional(),
11355
+ modalClosed: external_exports.boolean().optional(),
11356
+ loadingStarted: external_exports.boolean().optional(),
11357
+ loadingFinished: external_exports.boolean().optional(),
11358
+ textsAdded: external_exports.number().int().nonnegative().optional(),
11359
+ textsRemoved: external_exports.number().int().nonnegative().optional(),
11360
+ buttonsAddedIndices: external_exports.array(external_exports.number().int().nonnegative()).max(12).optional(),
11361
+ buttonsRemoved: external_exports.number().int().nonnegative().optional(),
11362
+ visibleRowsDelta: external_exports.number().int().optional(),
11363
+ totalRowsDelta: external_exports.number().int().optional(),
11364
+ fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
11365
+ fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
11366
+ invalidAppeared: external_exports.boolean().optional(),
11367
+ unchanged: external_exports.boolean().optional()
11368
+ }).passthrough().optional(),
9708
11369
  /**
9709
11370
  * How many of this screen's controls the client could name, split by
9710
11371
  * `identityProvenance`. Diagnostic only — the relay never grounds an
@@ -9723,6 +11384,10 @@ var agentSnapshotSchema = external_exports.object({
9723
11384
  }).passthrough().optional()
9724
11385
  }).passthrough();
9725
11386
  var agentContextSchema = external_exports.object({
11387
+ missionProtocol: external_exports.literal(1).optional(),
11388
+ missionId: boundedString(160).optional(),
11389
+ /** Preferred supported device language, reported by the SDK independently of map/UI labels. */
11390
+ deviceLocale: external_exports.enum(SUPPORTED_APPILOTS_LOCALES).optional(),
9726
11391
  /**
9727
11392
  * Client platform this observation was captured on. Optional and
9728
11393
  * additive (see `clientPlatformSchema`) — absent means
@@ -9738,7 +11403,19 @@ var agentContextSchema = external_exports.object({
9738
11403
  rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
9739
11404
  currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
9740
11405
  routeNames: external_exports.array(boundedString(200)).max(500).optional(),
9741
- canGoBack: external_exports.boolean().optional()
11406
+ canGoBack: external_exports.boolean().optional(),
11407
+ /**
11408
+ * The stack a back press pops through, oldest first, ending on
11409
+ * the current screen. `canGoBack` says a back exists; this says
11410
+ * where it goes.
11411
+ */
11412
+ backStack: external_exports.array(boundedString(200)).max(50).optional(),
11413
+ /**
11414
+ * Screens the session has been on, oldest first. Names only —
11415
+ * params carry record ids and often personal data, and a route
11416
+ * name is structure.
11417
+ */
11418
+ visited: external_exports.array(boundedString(200)).max(8).optional()
9742
11419
  }).passthrough().optional(),
9743
11420
  screenMetadata: external_exports.object({
9744
11421
  name: boundedString(200).optional(),
@@ -9844,7 +11521,7 @@ var formFillPayloadSchema = external_exports.object({
9844
11521
  submitAfterFill: external_exports.boolean().optional()
9845
11522
  }).passthrough();
9846
11523
  var uiInteractionPayloadSchema = external_exports.object({
9847
- action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
11524
+ action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
9848
11525
  // `targetId` is the canonical server/LLM field. SDK runtimes still
9849
11526
  // accept `componentId` as a compatibility alias.
9850
11527
  targetId: external_exports.string().min(1),
@@ -9961,6 +11638,12 @@ var actionDiagnoseSchema = external_exports.object({
9961
11638
  requiresUserInput: external_exports.boolean().optional()
9962
11639
  });
9963
11640
  var actionResultSchema = external_exports.object({
11641
+ nativeConfirmation: external_exports.object({
11642
+ title: external_exports.string().max(300),
11643
+ message: external_exports.string().max(1e3).optional(),
11644
+ buttonLabel: external_exports.string().max(160).optional(),
11645
+ handlerCompleted: external_exports.boolean()
11646
+ }).optional(),
9964
11647
  actionId: external_exports.string(),
9965
11648
  type: external_exports.string(),
9966
11649
  success: external_exports.boolean(),
@@ -10012,8 +11695,15 @@ var continueAgentSchema = external_exports.object({
10012
11695
  // issue #169 — the observation the server grounds targets against, now
10013
11696
  // validated + bounded at the border instead of z.record(z.unknown()).
10014
11697
  context: agentContextSchema.optional(),
10015
- /** Hop counter — server enforces a cap to prevent runaway loops */
10016
- hop: external_exports.number().int().min(1).max(10).optional()
11698
+ /**
11699
+ * Hop counter — server enforces a cap to prevent runaway loops.
11700
+ * The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
11701
+ * #503, apps/api/src/modules/agents/routes.ts): the first over-budget
11702
+ * hop has to get through validation so the route can answer it with
11703
+ * the friendly automation-limit message instead of a 422. The +3
11704
+ * headroom mirrors what 7/10 was before the recalibration.
11705
+ */
11706
+ hop: external_exports.number().int().min(1).max(13).optional()
10017
11707
  });
10018
11708
  var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
10019
11709
  var screenPermissionSchema = external_exports.object({
@@ -10081,7 +11771,7 @@ var updateProjectBudgetSchema = external_exports.object({
10081
11771
  /** null = remove webhook, undefined = leave unchanged. */
10082
11772
  budgetWebhookUrl: external_exports.string().url().nullable().optional()
10083
11773
  }).strict();
10084
- var localeSchema = external_exports.enum(["pt-BR", "en", "es"]);
11774
+ var localeSchema = external_exports.enum(SUPPORTED_APPILOTS_LOCALES);
10085
11775
  var hexColorSchema = external_exports.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
10086
11776
  message: "Must be a hex color like #6366f1"
10087
11777
  });
@@ -10119,15 +11809,18 @@ var themeTokensSchema = external_exports.object({
10119
11809
  */
10120
11810
  mode: external_exports.enum(["auto", "light", "dark"]).optional()
10121
11811
  }).strict();
11812
+ var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
11813
+ message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
11814
+ });
10122
11815
  var projectPersonalizationSchema = external_exports.object({
10123
11816
  /** Tone / persona instructions appended to the system prompt. */
10124
11817
  personaPrompt: external_exports.string().max(2048).nullable(),
10125
11818
  /** Display name in the chat header (e.g. "Aria"). */
10126
11819
  assistantName: external_exports.string().max(60).nullable(),
10127
11820
  /** URL or remote asset id for the avatar shown next to assistant turns. */
10128
- assistantAvatar: external_exports.string().max(500).nullable(),
11821
+ assistantAvatar: imageSourceField.nullable(),
10129
11822
  /** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
10130
- emptyStateIcon: external_exports.string().max(500).nullable(),
11823
+ emptyStateIcon: imageSourceField.nullable(),
10131
11824
  /** First-message text shown in the empty state. */
10132
11825
  welcomeMessage: external_exports.string().max(500).nullable(),
10133
11826
  /** Title rendered at the top of the chat. */
@@ -10141,20 +11834,20 @@ var projectPersonalizationSchema = external_exports.object({
10141
11834
  /** Chat-open FAB background color. Null falls back to theme.colors.primary. */
10142
11835
  triggerButtonColor: hexColorSchema.nullable(),
10143
11836
  /** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
10144
- triggerButtonImageUrl: external_exports.string().max(500).nullable()
11837
+ triggerButtonImageUrl: imageSourceField.nullable()
10145
11838
  });
10146
11839
  var updateProjectPersonalizationSchema = external_exports.object({
10147
11840
  personaPrompt: external_exports.string().max(2048).nullable().optional(),
10148
11841
  assistantName: external_exports.string().max(60).nullable().optional(),
10149
- assistantAvatar: external_exports.string().max(500).nullable().optional(),
10150
- emptyStateIcon: external_exports.string().max(500).nullable().optional(),
11842
+ assistantAvatar: imageSourceField.nullable().optional(),
11843
+ emptyStateIcon: imageSourceField.nullable().optional(),
10151
11844
  welcomeMessage: external_exports.string().max(500).nullable().optional(),
10152
11845
  chatTitle: external_exports.string().max(60).nullable().optional(),
10153
11846
  poweredByVisible: external_exports.boolean().optional(),
10154
11847
  theme: themeTokensSchema.nullable().optional(),
10155
11848
  defaultLocale: localeSchema.nullable().optional(),
10156
11849
  triggerButtonColor: hexColorSchema.nullable().optional(),
10157
- triggerButtonImageUrl: external_exports.string().max(500).nullable().optional()
11850
+ triggerButtonImageUrl: imageSourceField.nullable().optional()
10158
11851
  }).strict();
10159
11852
  var sandboxObservationSchema = external_exports.object({
10160
11853
  route: external_exports.string().max(200).optional(),
@@ -10174,7 +11867,19 @@ var sandboxObservationSchema = external_exports.object({
10174
11867
  type: external_exports.string().max(40).optional(),
10175
11868
  placeholder: external_exports.string().max(200).optional(),
10176
11869
  /** Inline validation state (SDK snapshot field). */
10177
- invalid: external_exports.boolean().optional()
11870
+ invalid: external_exports.boolean().optional(),
11871
+ /**
11872
+ * Whether the field holds anything, as its own fact rather than
11873
+ * an inference over `value`.
11874
+ *
11875
+ * Here because the eval posts through this schema: a fact the
11876
+ * SDK produces and this contract has no word for is a fact the
11877
+ * corpus can never grade the agent on. `invalid` and `required`
11878
+ * were already here; these two were the half that was missing.
11879
+ */
11880
+ filled: external_exports.boolean().optional(),
11881
+ /** This input has keyboard focus right now. */
11882
+ focused: external_exports.boolean().optional()
10178
11883
  })
10179
11884
  ).max(100).optional(),
10180
11885
  buttons: external_exports.array(
@@ -10632,7 +12337,7 @@ var manifestSchema = external_exports.object({
10632
12337
  navigation: navigationGraphSchema.partial().optional()
10633
12338
  }).passthrough();
10634
12339
  async function loadManifest(rootDir, manifestPath) {
10635
- const resolvedPath = import_node_path2.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
12340
+ const resolvedPath = import_node_path4.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
10636
12341
  let raw;
10637
12342
  try {
10638
12343
  raw = await (0, import_promises3.readFile)(resolvedPath, "utf-8");
@@ -10781,7 +12486,7 @@ var MCPGenerator = class _MCPGenerator {
10781
12486
  const agentReadyScreens = mergedScreens.map(
10782
12487
  (screen) => enrichScreenForAgent({
10783
12488
  ...screen,
10784
- filePath: screen.filePath ? import_node_path3.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
12489
+ filePath: screen.filePath ? import_node_path5.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
10785
12490
  })
10786
12491
  );
10787
12492
  const projectInfo = await this.getProjectInfo();
@@ -10805,19 +12510,33 @@ var MCPGenerator = class _MCPGenerator {
10805
12510
  }
10806
12511
  };
10807
12512
  const serialized = JSON.stringify(document, null, 2);
10808
- const checksum = this.calculateChecksum(serialized);
10809
- const filePath = import_node_path3.default.resolve(outputDir, `mcp-document.${this.options.format}`);
12513
+ const checksum = this.calculateChecksum(serializeForChecksum(document));
12514
+ const filePath = import_node_path5.default.resolve(outputDir, `mcp-document.${this.options.format}`);
10810
12515
  await (0, import_promises4.writeFile)(filePath, serialized, "utf-8");
10811
12516
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10812
- const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
12517
+ const controlFiles = analyzed.controlEvidenceFiles ?? {};
12518
+ await (0, import_promises4.writeFile)(
12519
+ import_node_path5.default.resolve(outputDir, "control-evidence.json"),
12520
+ JSON.stringify({ version: 1, files: controlFiles }, null, 2),
12521
+ "utf-8"
12522
+ );
12523
+ const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
10813
12524
  await (0, import_promises4.writeFile)(checksumFilePath, checksum, "utf-8");
10814
12525
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
12526
+ const evidenceCount = Object.values(controlFiles).reduce(
12527
+ (sum, entries) => sum + entries.length,
12528
+ 0
12529
+ );
12530
+ console.log(
12531
+ `[MCPGenerator] Source evidence: ${evidenceCount} icon controls across ${Object.keys(controlFiles).length} files (runtime binding required)`
12532
+ );
10815
12533
  console.log("[MCPGenerator] Generation complete!");
10816
12534
  return {
10817
12535
  document,
10818
12536
  filePath,
10819
12537
  format: this.options.format,
10820
- checksum
12538
+ checksum,
12539
+ ...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
10821
12540
  };
10822
12541
  }
10823
12542
  /**
@@ -10829,7 +12548,7 @@ var MCPGenerator = class _MCPGenerator {
10829
12548
  * after calling `generate()`.
10830
12549
  */
10831
12550
  static async readPreviousChecksum(outputDir) {
10832
- const checksumFilePath = import_node_path3.default.resolve(outputDir, ".appilots-checksum");
12551
+ const checksumFilePath = import_node_path5.default.resolve(outputDir, ".appilots-checksum");
10833
12552
  try {
10834
12553
  const content = await (0, import_promises4.readFile)(checksumFilePath, "utf-8");
10835
12554
  return content.trim() || null;
@@ -10841,14 +12560,14 @@ var MCPGenerator = class _MCPGenerator {
10841
12560
  * Calculate SHA-256 checksum of content
10842
12561
  */
10843
12562
  calculateChecksum(content) {
10844
- return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
12563
+ return (0, import_node_crypto2.createHash)("sha256").update(content).digest("hex");
10845
12564
  }
10846
12565
  /**
10847
12566
  * Get project name and version from package.json in rootDir
10848
12567
  */
10849
12568
  async getProjectInfo() {
10850
12569
  try {
10851
- const packageJsonPath = import_node_path3.default.resolve(this.analyzerConfig.rootDir, "package.json");
12570
+ const packageJsonPath = import_node_path5.default.resolve(this.analyzerConfig.rootDir, "package.json");
10852
12571
  const packageJsonContent = await (0, import_promises4.readFile)(packageJsonPath, "utf-8");
10853
12572
  const packageJson = JSON.parse(packageJsonContent);
10854
12573
  return {
@@ -10923,8 +12642,8 @@ function reportMetadataLint(document, strict) {
10923
12642
  }
10924
12643
 
10925
12644
  // src/cli/utils/remote-drift.ts
10926
- var import_node_fs = require("fs");
10927
- var import_node_path4 = require("path");
12645
+ var import_node_fs3 = require("fs");
12646
+ var import_node_path6 = require("path");
10928
12647
  var STUB_RATIO = 0.5;
10929
12648
  function compareWithRemote(local, remote) {
10930
12649
  if (remote.screensCount === local.screens) return null;
@@ -10937,7 +12656,7 @@ function compareWithRemote(local, remote) {
10937
12656
  }
10938
12657
  function readLocalDocumentSummary(outputDir) {
10939
12658
  try {
10940
- const raw = (0, import_node_fs.readFileSync)((0, import_node_path4.join)(outputDir, "mcp-document.json"), "utf-8");
12659
+ const raw = (0, import_node_fs3.readFileSync)((0, import_node_path6.join)(outputDir, "mcp-document.json"), "utf-8");
10941
12660
  const parsed = JSON.parse(raw);
10942
12661
  if (typeof parsed?.metadata?.totalScreens !== "number") return null;
10943
12662
  return { screens: parsed.metadata.totalScreens };
@@ -11022,14 +12741,18 @@ function assertDocumentHasScreens(document, options = {}) {
11022
12741
  }
11023
12742
  error("No screens found \u2014 this application map would give the agent nothing to navigate.");
11024
12743
  info("");
11025
- info("The analyzer looks for React Navigation: a <NavigationContainer> and");
11026
- info("<Stack.Screen> / <Tab.Screen> declarations. It found none.");
12744
+ info("The analyzer reads React Navigation \u2014 <X.Navigator> JSX and the static");
12745
+ info("`createXNavigator({ screens })` config \u2014 and Expo Router directory trees.");
12746
+ info("It found no screen in any of them.");
11027
12747
  info("");
11028
12748
  info("The two usual causes:");
11029
- info(" \u2022 The app uses Expo Router (or another file-based router). It is the");
11030
- info(" default for `npx create-expo-app` and the analyzer cannot read it.");
11031
- info(" \u2022 The screens live outside the analyzed paths \u2014 check `include`,");
11032
- info(" `exclude` and `screenPatterns` in your Appilots config.");
12749
+ info(" \u2022 The screens live outside the analyzed paths, or are named in a way");
12750
+ info(" the screen filter does not match \u2014 check `include`, `exclude` and");
12751
+ info(" `screenPatterns` in your Appilots config. The scan summary printed");
12752
+ info(" above says how many files were reached and how many were filtered.");
12753
+ info(" \u2022 The app uses a router the analyzer does not read yet, such as");
12754
+ info(" react-native-navigation (Wix). Declaring the screens works for any");
12755
+ info(" router \u2014 see the escape hatch below.");
11033
12756
  info("");
11034
12757
  info("The escape hatch, which works for any router: declare your screens in");
11035
12758
  info("an `appilots.manifest.json` at the project root. Its screens are merged");
@@ -11076,7 +12799,7 @@ function syncCommand() {
11076
12799
  const outputDir = config.outputDir || ".appilots";
11077
12800
  let previousChecksum = "";
11078
12801
  try {
11079
- const checksumPath = (0, import_node_path5.join)(outputDir, ".appilots-checksum");
12802
+ const checksumPath = (0, import_node_path7.join)(outputDir, ".appilots-checksum");
11080
12803
  previousChecksum = (await (0, import_promises5.readFile)(checksumPath, "utf-8")).trim();
11081
12804
  } catch {
11082
12805
  }
@@ -11203,7 +12926,7 @@ function syncCommand() {
11203
12926
 
11204
12927
  // src/cli/commands/watch.ts
11205
12928
  var import_commander3 = require("commander");
11206
- var import_node_fs2 = require("fs");
12929
+ var import_node_fs4 = require("fs");
11207
12930
  function watchCommand() {
11208
12931
  return new import_commander3.Command("watch").description("Watch project for changes and auto-sync MCP documents").option("--verbose", "Enable verbose logging", false).action(async (options) => {
11209
12932
  try {
@@ -11250,7 +12973,7 @@ function watchCommand() {
11250
12973
  dim("Press Ctrl+C to stop\n");
11251
12974
  let debounceTimer = null;
11252
12975
  const DEBOUNCE_MS = 500;
11253
- const watcher = (0, import_node_fs2.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
12976
+ const watcher = (0, import_node_fs4.watch)(process.cwd(), { recursive: true }, async (eventType, filename) => {
11254
12977
  if (!filename) return;
11255
12978
  if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(config.outputDir || ".appilots")) {
11256
12979
  return;
@@ -11302,6 +13025,95 @@ function watchCommand() {
11302
13025
 
11303
13026
  // src/cli/commands/generate.ts
11304
13027
  var import_commander4 = require("commander");
13028
+
13029
+ // src/cli/utils/reach-report.ts
13030
+ function actionableScreens(document) {
13031
+ return (document.screens ?? []).filter(
13032
+ (screen) => (screen.actions?.length ?? 0) > 0 || (screen.forms?.length ?? 0) > 0 || (screen.targets?.length ?? 0) > 0
13033
+ ).length;
13034
+ }
13035
+ function pct(part, whole) {
13036
+ if (whole <= 0) return "\u2014";
13037
+ return `${Math.round(100 * part / whole)}%`;
13038
+ }
13039
+ function reportReach(input) {
13040
+ const { document, navigation } = input;
13041
+ const screens = document.screens?.length ?? 0;
13042
+ const useful = actionableScreens(document);
13043
+ const filteredOut = document.metadata?.screensFilteredOut ?? 0;
13044
+ info("");
13045
+ info("Alcance da varredura");
13046
+ info(
13047
+ ` telas ${screens} de ${document.metadata?.analyzedFiles ?? 0} arquivos analisados` + (filteredOut > 0 ? `, ${filteredOut} fora pelo filtro de tela` : "")
13048
+ );
13049
+ info(` com aford\xE2ncia ${useful} (${pct(useful, screens)}) \u2014 a\xE7\xE3o, formul\xE1rio ou target`);
13050
+ if (navigation) {
13051
+ info(
13052
+ ` navega\xE7\xE3o ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos com evid\xEAncia, ${navigation.navigatorsWithJsx} navegador(es) no documento`
13053
+ );
13054
+ info(
13055
+ ` rotas ${navigation.routes}, ${navigation.routesLinkedToFile} (${pct(navigation.routesLinkedToFile, navigation.routes)}) ligadas a um arquivo de tela`
13056
+ );
13057
+ if (navigation.fileBasedRouter) {
13058
+ info(
13059
+ ` roteador ${navigation.fileBasedRouter.kind} em "${navigation.fileBasedRouter.routeDir}"`
13060
+ );
13061
+ }
13062
+ }
13063
+ const notes = [];
13064
+ if (navigation && navigation.routes > 0 && useful > navigation.routes * 5) {
13065
+ notes.push([
13066
+ `${useful} tela(s) com aford\xE2ncia para apenas ${navigation.routes} rota(s).`,
13067
+ "A varredura achou o que tocar em muito mais lugares do que sabe alcan\xE7ar.",
13068
+ `S\xF3 ${navigation.filesWithEvidence} de ${navigation.filesScanned} arquivos varridos mencionam navega\xE7\xE3o \u2014`,
13069
+ "se o roteador deste app mora fora deles, `navigationInclude` no `.appilotsrc`",
13070
+ "estende a varredura sem mexer no resto."
13071
+ ]);
13072
+ }
13073
+ if (navigation && navigation.routes === 0 && useful > 0) {
13074
+ notes.push([
13075
+ `${useful} tela(s) com aford\xE2ncia e nenhuma rota: o agente v\xEA o que tocar e n\xE3o sabe chegar l\xE1.`,
13076
+ "O extractor l\xEA React Navigation (JSX e configura\xE7\xE3o est\xE1tica) e expo-router.",
13077
+ "Se o app usa outro roteador, declare as telas em `appilots.manifest.json` \u2014",
13078
+ "elas s\xE3o mescladas por cima do que a an\xE1lise achou."
13079
+ ]);
13080
+ } else if (navigation && navigation.routes > 0 && navigation.routesLinkedToFile < navigation.routes / 2) {
13081
+ notes.push([
13082
+ `${navigation.routes - navigation.routesLinkedToFile} de ${navigation.routes} rotas n\xE3o dizem qual arquivo montam.`,
13083
+ "A liga\xE7\xE3o vem de `component={X}`. Formas indiretas \u2014 `getComponent={() => X}`,",
13084
+ "componente constru\xEDdo em runtime \u2014 n\xE3o resolvem, e o agente fica com destinos vazios."
13085
+ ]);
13086
+ }
13087
+ if (screens > 0 && useful === 0) {
13088
+ notes.push([
13089
+ `${screens} tela(s) encontradas e nenhuma com aford\xE2ncia.`,
13090
+ "O extractor mira `onPress`/`onChangeText` com um locator endere\xE7\xE1vel:",
13091
+ "`testID`, `appilotsId`, `accessibilityLabel` ou r\xF3tulo literal.",
13092
+ "Sem nenhum deles n\xE3o h\xE1 como o agente apontar para o elemento."
13093
+ ]);
13094
+ }
13095
+ if (filteredOut > 0 && screens > 0 && filteredOut > screens * 9) {
13096
+ notes.push([
13097
+ `${filteredOut} arquivos ficaram fora do filtro de tela para ${screens} dentro.`,
13098
+ "Se as telas deste app moram noutro lugar, uma linha de `screenPatterns` no",
13099
+ "`.appilotsrc` aponta onde \u2014 \xE9 o degrau mais barato da escada."
13100
+ ]);
13101
+ }
13102
+ if (navigation && navigation.filesScanned > 0 && navigation.filesWithEvidence === 0) {
13103
+ notes.push([
13104
+ `Nenhum dos ${navigation.filesScanned} arquivos varridos menciona navega\xE7\xE3o.`,
13105
+ "Confira `include`/`exclude` no `.appilotsrc`: o extractor pode n\xE3o estar",
13106
+ "alcan\xE7ando o diret\xF3rio onde o roteador \xE9 declarado."
13107
+ ]);
13108
+ }
13109
+ for (const note of notes) {
13110
+ info("");
13111
+ warn(note[0]);
13112
+ for (const line of note.slice(1)) info(` ${line}`);
13113
+ }
13114
+ }
13115
+
13116
+ // src/cli/commands/generate.ts
11305
13117
  function generateCommand() {
11306
13118
  return new import_commander4.Command("generate").description("Generate MCP documents from project source (local only)").option("-o, --output <path>", "Output directory for MCP documents", ".appilots").option("-f, --format <format>", "Output format (only json supported)", "json").option(
11307
13119
  "--strict-metadata",
@@ -11318,7 +13130,7 @@ function generateCommand() {
11318
13130
  spinner.start();
11319
13131
  let loaded = null;
11320
13132
  try {
11321
- loaded = loadConfig(warn);
13133
+ loaded = loadConfig(warn, { requireApiKey: false });
11322
13134
  } catch {
11323
13135
  loaded = null;
11324
13136
  }
@@ -11326,11 +13138,21 @@ function generateCommand() {
11326
13138
  rootDir: process.cwd(),
11327
13139
  outputDir: options.output || ".appilots",
11328
13140
  format: "json",
13141
+ strictScreens: loaded?.strictScreens,
13142
+ screenPatterns: loaded?.screenPatterns,
13143
+ include: loaded?.include,
13144
+ exclude: loaded?.exclude,
13145
+ navigationInclude: loaded?.navigationInclude,
13146
+ navigationExclude: loaded?.navigationExclude,
11329
13147
  platform: loaded?.platform,
11330
13148
  manifestPath: loaded?.manifestPath
11331
13149
  });
11332
13150
  const output = await generator.generate();
11333
13151
  spinner.stop();
13152
+ reportReach({
13153
+ document: output.document,
13154
+ ...output.diagnostics?.navigation ? { navigation: output.diagnostics.navigation } : {}
13155
+ });
11334
13156
  assertDocumentHasScreens(output.document, {
11335
13157
  rootDir: process.cwd(),
11336
13158
  allowEmpty: Boolean(options.allowEmpty)
@@ -11350,20 +13172,19 @@ function generateCommand() {
11350
13172
 
11351
13173
  // src/cli/commands/annotate.ts
11352
13174
  var import_promises6 = require("fs/promises");
11353
- var import_node_path7 = __toESM(require("path"));
13175
+ var import_node_path9 = __toESM(require("path"));
11354
13176
  var import_commander5 = require("commander");
11355
- var import_fast_glob6 = __toESM(require("fast-glob"));
11356
13177
 
11357
13178
  // src/annotate/sites.ts
11358
- var import_traverse12 = __toESM(require("@babel/traverse"));
11359
- var t13 = __toESM(require("@babel/types"));
13179
+ var import_traverse13 = __toESM(require("@babel/traverse"));
13180
+ var t16 = __toESM(require("@babel/types"));
11360
13181
 
11361
13182
  // src/annotate/forwarding.ts
11362
- var import_node_path6 = __toESM(require("path"));
11363
- var import_node_fs3 = require("fs");
11364
- var import_traverse11 = __toESM(require("@babel/traverse"));
11365
- var t12 = __toESM(require("@babel/types"));
11366
- var traverse11 = import_traverse11.default.default ?? import_traverse11.default;
13183
+ var import_node_path8 = __toESM(require("path"));
13184
+ var import_node_fs5 = require("fs");
13185
+ var import_traverse12 = __toESM(require("@babel/traverse"));
13186
+ var t15 = __toESM(require("@babel/types"));
13187
+ var traverse12 = import_traverse12.default.default ?? import_traverse12.default;
11367
13188
  var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
11368
13189
  "View",
11369
13190
  "Text",
@@ -11390,14 +13211,14 @@ var RN_TESTID_CARRIERS = /* @__PURE__ */ new Set([
11390
13211
  ]);
11391
13212
  function collectImports(ast) {
11392
13213
  const out = /* @__PURE__ */ new Map();
11393
- traverse11(ast, {
13214
+ traverse12(ast, {
11394
13215
  ImportDeclaration(nodePath) {
11395
13216
  const source = nodePath.node.source.value;
11396
13217
  for (const spec of nodePath.node.specifiers) {
11397
- if (t12.isImportSpecifier(spec)) {
11398
- const imported = t12.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
13218
+ if (t15.isImportSpecifier(spec)) {
13219
+ const imported = t15.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
11399
13220
  out.set(spec.local.name, { source, local: spec.local.name, imported });
11400
- } else if (t12.isImportDefaultSpecifier(spec)) {
13221
+ } else if (t15.isImportDefaultSpecifier(spec)) {
11401
13222
  out.set(spec.local.name, { source, local: spec.local.name, imported: "default" });
11402
13223
  }
11403
13224
  }
@@ -11405,17 +13226,17 @@ function collectImports(ast) {
11405
13226
  });
11406
13227
  return out;
11407
13228
  }
11408
- var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
13229
+ var EXTENSIONS2 = [".tsx", ".ts", ".jsx", ".js"];
11409
13230
  function resolveRelativeImport(fromFile, source) {
11410
13231
  if (!source.startsWith(".")) return void 0;
11411
- const base = import_node_path6.default.resolve(import_node_path6.default.dirname(fromFile), source);
11412
- for (const ext of EXTENSIONS) {
13232
+ const base = import_node_path8.default.resolve(import_node_path8.default.dirname(fromFile), source);
13233
+ for (const ext of EXTENSIONS2) {
11413
13234
  const candidate = `${base}${ext}`;
11414
- if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
13235
+ if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
11415
13236
  }
11416
- for (const ext of EXTENSIONS) {
11417
- const candidate = import_node_path6.default.join(base, `index${ext}`);
11418
- if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
13237
+ for (const ext of EXTENSIONS2) {
13238
+ const candidate = import_node_path8.default.join(base, `index${ext}`);
13239
+ if ((0, import_node_fs5.existsSync)(candidate)) return candidate;
11419
13240
  }
11420
13241
  return void 0;
11421
13242
  }
@@ -11430,7 +13251,7 @@ function inspectModule(file2, exportName, depth = 0) {
11430
13251
  if (depth > 2) return none;
11431
13252
  let source;
11432
13253
  try {
11433
- source = (0, import_node_fs3.readFileSync)(file2, "utf-8");
13254
+ source = (0, import_node_fs5.readFileSync)(file2, "utf-8");
11434
13255
  } catch {
11435
13256
  return none;
11436
13257
  }
@@ -11445,13 +13266,13 @@ function inspectModule(file2, exportName, depth = 0) {
11445
13266
  let forwards = false;
11446
13267
  let derives = false;
11447
13268
  let barrelTarget;
11448
- traverse11(ast, {
13269
+ traverse12(ast, {
11449
13270
  // `export { Card } from './Card'` — follow one hop.
11450
13271
  ExportNamedDeclaration(nodePath) {
11451
13272
  const from = nodePath.node.source?.value;
11452
13273
  if (!from) return;
11453
13274
  const named = nodePath.node.specifiers.some(
11454
- (spec) => t12.isExportSpecifier(spec) && (t12.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
13275
+ (spec) => t15.isExportSpecifier(spec) && (t15.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === exportName
11455
13276
  );
11456
13277
  if (named) barrelTarget = from;
11457
13278
  },
@@ -11460,11 +13281,11 @@ function inspectModule(file2, exportName, depth = 0) {
11460
13281
  },
11461
13282
  // `interface CardProps { testID?: string }` / `type Props = { testID… }`
11462
13283
  TSPropertySignature(nodePath) {
11463
- if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
13284
+ if (t15.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11464
13285
  },
11465
13286
  // `function Card({ testID }) {}` — the destructured parameter.
11466
13287
  ObjectProperty(nodePath) {
11467
- if (t12.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
13288
+ if (t15.isIdentifier(nodePath.node.key) && nodePath.node.key.name === "testID") forwards = true;
11468
13289
  },
11469
13290
  // `<View {...props} />` — anything spread onto JSX carries it along.
11470
13291
  JSXSpreadAttribute() {
@@ -11475,17 +13296,17 @@ function inspectModule(file2, exportName, depth = 0) {
11475
13296
  // that is one hardcoded name shared by every instance, which is the
11476
13297
  // duplicate-id problem rather than a solution to it.
11477
13298
  JSXAttribute(nodePath) {
11478
- const name = t12.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
13299
+ const name = t15.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
11479
13300
  if (name !== "testID" && name !== "accessibilityLabel") return;
11480
13301
  const value = nodePath.node.value;
11481
- if (!t12.isJSXExpressionContainer(value)) return;
11482
- if (t12.isStringLiteral(value.expression)) return;
13302
+ if (!t15.isJSXExpressionContainer(value)) return;
13303
+ if (t15.isStringLiteral(value.expression)) return;
11483
13304
  derives = true;
11484
13305
  },
11485
13306
  // `useAppilotsTarget(`${groupId}-${option.value}`, …)`
11486
13307
  CallExpression(nodePath) {
11487
13308
  const callee = nodePath.node.callee;
11488
- if (t12.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
13309
+ if (t15.isIdentifier(callee) && IDENTITY_HOOKS.has(callee.name)) derives = true;
11489
13310
  }
11490
13311
  });
11491
13312
  if (forwards || derives) return { forwards, derives };
@@ -11519,7 +13340,7 @@ function resetForwardingCache() {
11519
13340
  }
11520
13341
 
11521
13342
  // src/annotate/sites.ts
11522
- var traverse12 = import_traverse12.default.default ?? import_traverse12.default;
13343
+ var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
11523
13344
  var LABEL_PROPS = ["title", "label", "placeholder"];
11524
13345
  var IDENTITY_PROPS = ["testID", "appilotsId", "accessibilityLabel"];
11525
13346
  function kebab(text) {
@@ -11570,18 +13391,18 @@ function hasLabelText(element) {
11570
13391
  function namedHandler(element, prop) {
11571
13392
  if (!prop) return void 0;
11572
13393
  const attr = findJsxAttribute(element, prop);
11573
- if (!attr || !t13.isJSXExpressionContainer(attr.value)) return void 0;
11574
- return t13.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
13394
+ if (!attr || !t16.isJSXExpressionContainer(attr.value)) return void 0;
13395
+ return t16.isIdentifier(attr.value.expression) ? attr.value.expression.name : void 0;
11575
13396
  }
11576
13397
  var ANNOTATABLE_ROLES = /* @__PURE__ */ new Set(["button", "input", "select", "toggle", "date", "list"]);
11577
13398
  function isPerItemRender(nodePath) {
11578
13399
  let current = nodePath.parentPath;
11579
13400
  while (current) {
11580
13401
  const node = current.node;
11581
- if (t13.isCallExpression(node) && t13.isMemberExpression(node.callee) && t13.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
13402
+ if (t16.isCallExpression(node) && t16.isMemberExpression(node.callee) && t16.isIdentifier(node.callee.property) && (node.callee.property.name === "map" || node.callee.property.name === "flatMap")) {
11582
13403
  return true;
11583
13404
  }
11584
- if (t13.isJSXAttribute(node) && t13.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
13405
+ if (t16.isJSXAttribute(node) && t16.isJSXIdentifier(node.name) && /^render[A-Z]|^ListHeaderComponent$|^ListFooterComponent$/.test(node.name.name) && node.name.name !== "renderScrollComponent") {
11585
13406
  return /^renderItem$|^renderSectionHeader$|^renderSectionFooter$/.test(node.name.name);
11586
13407
  }
11587
13408
  current = current.parentPath;
@@ -11600,7 +13421,7 @@ function findAnnotationSites(source, file2) {
11600
13421
  const sites = [];
11601
13422
  const perComponent = /* @__PURE__ */ new Map();
11602
13423
  const usedIds = /* @__PURE__ */ new Set();
11603
- traverse12(ast, {
13424
+ traverse13(ast, {
11604
13425
  JSXOpeningElement(nodePath) {
11605
13426
  for (const prop of IDENTITY_PROPS) {
11606
13427
  const existing = getStringAttr(nodePath.node, prop);
@@ -11608,10 +13429,10 @@ function findAnnotationSites(source, file2) {
11608
13429
  }
11609
13430
  }
11610
13431
  });
11611
- traverse12(ast, {
13432
+ traverse13(ast, {
11612
13433
  JSXOpeningElement(nodePath) {
11613
13434
  const element = nodePath.node;
11614
- if (!t13.isJSXIdentifier(element.name)) return;
13435
+ if (!t16.isJSXIdentifier(element.name)) return;
11615
13436
  const component = element.name.name;
11616
13437
  const role = classifyJsxComponent(component, element);
11617
13438
  if (!ANNOTATABLE_ROLES.has(role)) return;
@@ -11693,23 +13514,23 @@ function applyToFile(source, sites) {
11693
13514
  }
11694
13515
 
11695
13516
  // src/annotate/forward.ts
11696
- var import_node_fs4 = require("fs");
13517
+ var import_node_fs6 = require("fs");
11697
13518
  var import_magic_string2 = __toESM(require("magic-string"));
11698
- var import_traverse13 = __toESM(require("@babel/traverse"));
11699
- var t14 = __toESM(require("@babel/types"));
11700
- var traverse13 = import_traverse13.default.default ?? import_traverse13.default;
13519
+ var import_traverse14 = __toESM(require("@babel/traverse"));
13520
+ var t17 = __toESM(require("@babel/types"));
13521
+ var traverse14 = import_traverse14.default.default ?? import_traverse14.default;
11701
13522
  function findComponent(ast, name) {
11702
13523
  let found;
11703
- traverse13(ast, {
13524
+ traverse14(ast, {
11704
13525
  FunctionDeclaration(nodePath) {
11705
13526
  if (nodePath.node.id?.name === name) {
11706
13527
  found ??= { params: nodePath.node.params, body: nodePath.node.body };
11707
13528
  }
11708
13529
  },
11709
13530
  VariableDeclarator(nodePath) {
11710
- if (!t14.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
13531
+ if (!t17.isIdentifier(nodePath.node.id) || nodePath.node.id.name !== name) return;
11711
13532
  const init = nodePath.node.init;
11712
- if (t14.isArrowFunctionExpression(init) || t14.isFunctionExpression(init)) {
13533
+ if (t17.isArrowFunctionExpression(init) || t17.isFunctionExpression(init)) {
11713
13534
  found ??= { params: init.params, body: init.body };
11714
13535
  }
11715
13536
  }
@@ -11721,26 +13542,26 @@ function findReturnedRoots(body) {
11721
13542
  let unsupported = false;
11722
13543
  const record = (node) => {
11723
13544
  if (!node) return;
11724
- if (t14.isParenthesizedExpression(node)) return record(node.expression);
11725
- if (t14.isJSXElement(node)) {
13545
+ if (t17.isParenthesizedExpression(node)) return record(node.expression);
13546
+ if (t17.isJSXElement(node)) {
11726
13547
  roots.push(node.openingElement);
11727
13548
  return;
11728
13549
  }
11729
- if (t14.isConditionalExpression(node)) {
13550
+ if (t17.isConditionalExpression(node)) {
11730
13551
  record(node.consequent);
11731
13552
  record(node.alternate);
11732
13553
  return;
11733
13554
  }
11734
- if (t14.isJSXFragment(node)) {
13555
+ if (t17.isJSXFragment(node)) {
11735
13556
  unsupported = true;
11736
13557
  return;
11737
13558
  }
11738
- if (t14.isNullLiteral(node)) return;
13559
+ if (t17.isNullLiteral(node)) return;
11739
13560
  unsupported = true;
11740
13561
  };
11741
- if (t14.isBlockStatement(body)) {
11742
- traverse13(
11743
- t14.file(t14.program([t14.expressionStatement(t14.functionExpression(null, [], body))])),
13562
+ if (t17.isBlockStatement(body)) {
13563
+ traverse14(
13564
+ t17.file(t17.program([t17.expressionStatement(t17.functionExpression(null, [], body))])),
11744
13565
  {
11745
13566
  ReturnStatement(nodePath) {
11746
13567
  record(nodePath.node.argument);
@@ -11754,7 +13575,7 @@ function findReturnedRoots(body) {
11754
13575
  return roots;
11755
13576
  }
11756
13577
  function isCarrierRoot(element, imports) {
11757
- if (!t14.isJSXIdentifier(element.name)) return false;
13578
+ if (!t17.isJSXIdentifier(element.name)) return false;
11758
13579
  const name = element.name.name;
11759
13580
  const origin = imports.get(name);
11760
13581
  if (origin?.source !== "react-native") return false;
@@ -11762,7 +13583,7 @@ function isCarrierRoot(element, imports) {
11762
13583
  }
11763
13584
  function hasProp(element, name) {
11764
13585
  return element.attributes.some(
11765
- (attr) => t14.isJSXAttribute(attr) && t14.isJSXIdentifier(attr.name) && attr.name.name === name
13586
+ (attr) => t17.isJSXAttribute(attr) && t17.isJSXIdentifier(attr.name) && attr.name.name === name
11766
13587
  );
11767
13588
  }
11768
13589
  function indentOf(source, offset) {
@@ -11788,12 +13609,12 @@ ${indent}${text}` };
11788
13609
  }
11789
13610
  function followBarrel(file2, ast, component) {
11790
13611
  let target;
11791
- traverse13(ast, {
13612
+ traverse14(ast, {
11792
13613
  ExportNamedDeclaration(nodePath) {
11793
13614
  const from = nodePath.node.source?.value;
11794
13615
  if (!from) return;
11795
13616
  const named = nodePath.node.specifiers.some(
11796
- (spec) => t14.isExportSpecifier(spec) && (t14.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
13617
+ (spec) => t17.isExportSpecifier(spec) && (t17.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value) === component
11797
13618
  );
11798
13619
  if (named) target ??= from;
11799
13620
  }
@@ -11804,7 +13625,7 @@ function planForward(file2, component, depth = 0) {
11804
13625
  const base = { file: file2, component, edits: [], roots: 0 };
11805
13626
  let source;
11806
13627
  try {
11807
- source = (0, import_node_fs4.readFileSync)(file2, "utf-8");
13628
+ source = (0, import_node_fs6.readFileSync)(file2, "utf-8");
11808
13629
  } catch {
11809
13630
  return { ...base, refusal: `cannot read ${file2}` };
11810
13631
  }
@@ -11821,7 +13642,7 @@ function planForward(file2, component, depth = 0) {
11821
13642
  return { ...base, refusal: `no function named ${component} in this file` };
11822
13643
  }
11823
13644
  const param = found.params[0];
11824
- if (!t14.isObjectPattern(param)) {
13645
+ if (!t17.isObjectPattern(param)) {
11825
13646
  return {
11826
13647
  ...base,
11827
13648
  refusal: `${component} does not destructure its props, so there is no safe place to add one without knowing what the parameter is called downstream`
@@ -11837,7 +13658,7 @@ function planForward(file2, component, depth = 0) {
11837
13658
  const imports = collectImports(ast);
11838
13659
  const nonCarrier = roots.find((root) => !isCarrierRoot(root, imports));
11839
13660
  if (nonCarrier) {
11840
- const name = t14.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
13661
+ const name = t17.isJSXIdentifier(nonCarrier.name) ? nonCarrier.name.name : "its root";
11841
13662
  return {
11842
13663
  ...base,
11843
13664
  refusal: `${component} renders <${name}> at its root, which is not a React Native element known to accept testID. Forwarding into it would move the problem down a level, not fix it`
@@ -11845,30 +13666,30 @@ function planForward(file2, component, depth = 0) {
11845
13666
  }
11846
13667
  const edits = [];
11847
13668
  const alreadyDestructured = param.properties.some(
11848
- (prop) => t14.isObjectProperty(prop) && t14.isIdentifier(prop.key) && prop.key.name === "testID"
13669
+ (prop) => t17.isObjectProperty(prop) && t17.isIdentifier(prop.key) && prop.key.name === "testID"
11849
13670
  );
11850
13671
  if (!alreadyDestructured) {
11851
13672
  const insert = insertAfterLastMember(source, param.properties, "testID", param.end ?? 0, ",");
11852
13673
  edits.push({ ...insert, what: "parameter" });
11853
13674
  }
11854
13675
  const annotation = param.typeAnnotation;
11855
- if (t14.isTSTypeAnnotation(annotation)) {
13676
+ if (t17.isTSTypeAnnotation(annotation)) {
11856
13677
  const declared = annotation.typeAnnotation;
11857
13678
  let body;
11858
13679
  let closing = 0;
11859
- if (t14.isTSTypeLiteral(declared)) {
13680
+ if (t17.isTSTypeLiteral(declared)) {
11860
13681
  body = declared.members;
11861
13682
  closing = declared.end ?? 0;
11862
- } else if (t14.isTSTypeReference(declared) && t14.isIdentifier(declared.typeName)) {
13683
+ } else if (t17.isTSTypeReference(declared) && t17.isIdentifier(declared.typeName)) {
11863
13684
  const typeName = declared.typeName.name;
11864
- traverse13(ast, {
13685
+ traverse14(ast, {
11865
13686
  TSInterfaceDeclaration(nodePath) {
11866
13687
  if (nodePath.node.id.name !== typeName) return;
11867
13688
  body = nodePath.node.body.body;
11868
13689
  closing = nodePath.node.body.end ?? 0;
11869
13690
  },
11870
13691
  TSTypeAliasDeclaration(nodePath) {
11871
- if (nodePath.node.id.name !== typeName || !t14.isTSTypeLiteral(nodePath.node.typeAnnotation))
13692
+ if (nodePath.node.id.name !== typeName || !t17.isTSTypeLiteral(nodePath.node.typeAnnotation))
11872
13693
  return;
11873
13694
  body = nodePath.node.typeAnnotation.members;
11874
13695
  closing = nodePath.node.typeAnnotation.end ?? 0;
@@ -11883,7 +13704,7 @@ function planForward(file2, component, depth = 0) {
11883
13704
  }
11884
13705
  if (body) {
11885
13706
  const declaresTestId = body.some(
11886
- (member) => t14.isTSPropertySignature(member) && t14.isIdentifier(member.key) && member.key.name === "testID"
13707
+ (member) => t17.isTSPropertySignature(member) && t17.isIdentifier(member.key) && member.key.name === "testID"
11887
13708
  );
11888
13709
  if (!declaresTestId) {
11889
13710
  const insert = insertAfterLastMember(source, body, "testID?: string;", closing, ";");
@@ -11940,7 +13761,7 @@ function annotateCommand() {
11940
13761
  const spinner = createSpinner("Scanning for unaddressable controls...");
11941
13762
  spinner.start();
11942
13763
  resetForwardingCache();
11943
- const files = await (0, import_fast_glob6.default)(include, { cwd: rootDir, ignore: exclude, absolute: true });
13764
+ const files = await globSorted(include, { cwd: rootDir, ignore: exclude, absolute: true });
11944
13765
  const scan = async () => {
11945
13766
  const out = [];
11946
13767
  for (const file2 of files) {
@@ -11989,9 +13810,9 @@ function annotateCommand() {
11989
13810
  written: options.write === true,
11990
13811
  annotated: written.length,
11991
13812
  blocked: blocked.length,
11992
- files: touched.map((edit) => import_node_path7.default.relative(rootDir, edit.file)),
13813
+ files: touched.map((edit) => import_node_path9.default.relative(rootDir, edit.file)),
11993
13814
  sites: [...written, ...blocked].map((site) => ({
11994
- file: import_node_path7.default.relative(rootDir, site.file),
13815
+ file: import_node_path9.default.relative(rootDir, site.file),
11995
13816
  line: site.line,
11996
13817
  component: site.component,
11997
13818
  role: site.role,
@@ -12034,9 +13855,12 @@ async function planForwards(edits, rootDir) {
12034
13855
  void rootDir;
12035
13856
  return plans;
12036
13857
  }
13858
+ function escapeRegExp(value) {
13859
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13860
+ }
12037
13861
  function findImportSource(source, name) {
12038
13862
  const pattern = new RegExp(
12039
- String.raw`import\s*\{[^}]*\b${name}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
13863
+ String.raw`import\s*\{[^}]*\b${escapeRegExp(name)}\b[^}]*\}\s*from\s*['"]([^'"]+)['"]`
12040
13864
  );
12041
13865
  return pattern.exec(source)?.[1];
12042
13866
  }
@@ -12045,7 +13869,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12045
13869
  if (written.length > 0) {
12046
13870
  heading(didWrite ? "Added" : "Would add");
12047
13871
  for (const edit of touched) {
12048
- info(import_node_path7.default.relative(rootDir, edit.file));
13872
+ info(import_node_path9.default.relative(rootDir, edit.file));
12049
13873
  for (const site of edit.written) {
12050
13874
  dim(` ${String(site.line).padStart(4)} <${site.component}> testID="${site.testId}"`);
12051
13875
  }
@@ -12057,7 +13881,7 @@ function reportSites(rootDir, touched, blocked, didWrite, forwards) {
12057
13881
  if (teachable.size > 0) {
12058
13882
  heading(didWrite ? "Taught to carry testID" : "Would teach to carry testID");
12059
13883
  for (const plan of teachable.values()) {
12060
- info(`<${plan.component}> ${import_node_path7.default.relative(rootDir, plan.file)}`);
13884
+ info(`<${plan.component}> ${import_node_path9.default.relative(rootDir, plan.file)}`);
12061
13885
  for (const edit of plan.edits) {
12062
13886
  dim(` ${edit.what}: ${edit.text.trim()}`);
12063
13887
  }
@@ -12365,10 +14189,10 @@ function matchActions(expected, actual, reply = "") {
12365
14189
  var import_fs9 = require("fs");
12366
14190
  var import_path8 = require("path");
12367
14191
  var EMPTY_BASELINE = { generatedAt: null, gitSha: null, perScenario: {} };
12368
- function loadBaseline(path11) {
12369
- if (!(0, import_fs9.existsSync)(path11)) return { ...EMPTY_BASELINE, perScenario: {} };
14192
+ function loadBaseline(path13) {
14193
+ if (!(0, import_fs9.existsSync)(path13)) return { ...EMPTY_BASELINE, perScenario: {} };
12370
14194
  try {
12371
- const raw = JSON.parse((0, import_fs9.readFileSync)(path11, "utf-8"));
14195
+ const raw = JSON.parse((0, import_fs9.readFileSync)(path13, "utf-8"));
12372
14196
  return {
12373
14197
  generatedAt: raw.generatedAt ?? null,
12374
14198
  gitSha: raw.gitSha ?? null,
@@ -12378,9 +14202,9 @@ function loadBaseline(path11) {
12378
14202
  return { ...EMPTY_BASELINE, perScenario: {} };
12379
14203
  }
12380
14204
  }
12381
- function saveBaseline(path11, baseline) {
12382
- (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path11), { recursive: true });
12383
- (0, import_fs9.writeFileSync)(path11, `${JSON.stringify(baseline, null, 2)}
14205
+ function saveBaseline(path13, baseline) {
14206
+ (0, import_fs9.mkdirSync)((0, import_path8.dirname)(path13), { recursive: true });
14207
+ (0, import_fs9.writeFileSync)(path13, `${JSON.stringify(baseline, null, 2)}
12384
14208
  `, "utf-8");
12385
14209
  }
12386
14210
  function buildBaseline(outcomes, meta) {
@@ -12408,11 +14232,11 @@ function diffAgainstBaseline(outcomes, baseline, maxTokenRegression) {
12408
14232
  continue;
12409
14233
  }
12410
14234
  if (outcome.status === "passed" && prior.status === "passed" && prior.totalTokens > 0 && outcome.totalTokens > prior.totalTokens * (1 + maxTokenRegression)) {
12411
- const pct = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
14235
+ const pct2 = Math.round((outcome.totalTokens - prior.totalTokens) / prior.totalTokens * 100);
12412
14236
  regressions.push({
12413
14237
  name: outcome.name,
12414
14238
  kind: "token-regression",
12415
- detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
14239
+ detail: `tokens ${prior.totalTokens} \u2192 ${outcome.totalTokens} (+${pct2}%, threshold +${Math.round(maxTokenRegression * 100)}%)`
12416
14240
  });
12417
14241
  }
12418
14242
  }
@@ -12673,9 +14497,145 @@ Pass rate: ${passed}/${total} (${passRatePct}%) \u2014 threshold ${minPassRatePc
12673
14497
  });
12674
14498
  }
12675
14499
 
14500
+ // src/cli/commands/knowledge.ts
14501
+ var import_commander8 = require("commander");
14502
+ var import_promises7 = require("fs/promises");
14503
+ var import_node_path10 = require("path");
14504
+ var import_node_crypto3 = require("crypto");
14505
+ var import_fast_glob2 = __toESM(require("fast-glob"));
14506
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".html", ".htm", ".pdf"]);
14507
+ var MIME_MAP = {
14508
+ ".md": "text/markdown",
14509
+ ".txt": "text/plain",
14510
+ ".html": "text/html",
14511
+ ".htm": "text/html",
14512
+ ".pdf": "application/pdf"
14513
+ };
14514
+ function knowledgeCommand() {
14515
+ const cmd = new import_commander8.Command("knowledge").description("Manage the project knowledge base (RAG)");
14516
+ cmd.command("sync").description("Sync local documentation files to the knowledge base").option("--verbose", "Enable verbose logging", false).option("--api-key <key>", "Appilots API key (overrides .appilotsrc)").option("--server <url>", "Appilots server URL (overrides .appilotsrc)").option("--json", "Output a machine-readable JSON result (implies --quiet)", false).option("--quiet", "Suppress banner and informational output", false).action(async (options) => {
14517
+ if (options.json) setLogMode("json");
14518
+ else if (options.quiet) setLogMode("quiet");
14519
+ try {
14520
+ banner();
14521
+ const loaded = loadConfig(warn);
14522
+ const apiKey = options.apiKey || loaded?.apiKey;
14523
+ if (!apiKey) {
14524
+ error("No .appilotsrc configuration found");
14525
+ info("Run: appilots init --api-key <your-key>");
14526
+ jsonOutput({ success: false, error: "No configuration found (missing API key)" });
14527
+ process.exit(1);
14528
+ return;
14529
+ }
14530
+ const config = {
14531
+ serverUrl: DEFAULT_SERVER_URL,
14532
+ ...loaded,
14533
+ apiKey,
14534
+ ...options.server ? { serverUrl: options.server } : {}
14535
+ };
14536
+ const sources = config.knowledge?.sources;
14537
+ if (!sources || sources.length === 0) {
14538
+ error("No knowledge sources configured");
14539
+ info("Add knowledge.sources to .appilotsrc:");
14540
+ info(' { "knowledge": { "sources": ["docs/**/*.md"] } }');
14541
+ jsonOutput({ success: false, error: "No knowledge sources configured" });
14542
+ process.exit(1);
14543
+ return;
14544
+ }
14545
+ const spinner = createSpinner("Scanning for knowledge documents...");
14546
+ spinner.start();
14547
+ const rootDir = process.cwd();
14548
+ const excludePatterns = config.knowledge?.exclude ?? [];
14549
+ const allFiles = [];
14550
+ for (const pattern of sources) {
14551
+ const matches = await (0, import_fast_glob2.default)(pattern, {
14552
+ cwd: rootDir,
14553
+ ignore: ["node_modules/**", ...excludePatterns],
14554
+ onlyFiles: true
14555
+ });
14556
+ for (const match of matches) {
14557
+ const ext = (0, import_node_path10.extname)(match).toLowerCase();
14558
+ if (SUPPORTED_EXTENSIONS.has(ext) && !allFiles.includes(match)) {
14559
+ allFiles.push(match);
14560
+ }
14561
+ }
14562
+ }
14563
+ spinner.stop();
14564
+ if (allFiles.length === 0) {
14565
+ warn("No supported files found matching knowledge.sources patterns");
14566
+ info(`Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`);
14567
+ jsonOutput({ success: true, uploaded: 0, skipped: 0, errors: 0, files: 0 });
14568
+ return;
14569
+ }
14570
+ info(`Found ${allFiles.length} document${allFiles.length === 1 ? "" : "s"}`);
14571
+ const documents = [];
14572
+ for (const filePath of allFiles) {
14573
+ const absolutePath = (0, import_node_path10.resolve)(rootDir, filePath);
14574
+ const buffer = await (0, import_promises7.readFile)(absolutePath);
14575
+ const checksum = (0, import_node_crypto3.createHash)("sha256").update(buffer).digest("hex");
14576
+ const ext = (0, import_node_path10.extname)(filePath).toLowerCase();
14577
+ const mimeType = MIME_MAP[ext] ?? "text/plain";
14578
+ documents.push({
14579
+ filename: (0, import_node_path10.relative)(rootDir, absolutePath),
14580
+ content: buffer.toString("base64"),
14581
+ mimeType,
14582
+ checksum
14583
+ });
14584
+ }
14585
+ const uploadSpinner = createSpinner(
14586
+ `Uploading ${documents.length} document${documents.length === 1 ? "" : "s"}...`
14587
+ );
14588
+ uploadSpinner.start();
14589
+ const apiClient = new AppilotsAPIClient({
14590
+ serverUrl: config.serverUrl,
14591
+ apiKey: config.apiKey,
14592
+ timeoutMs: 12e4
14593
+ // Knowledge sync can take longer (embedding)
14594
+ });
14595
+ const result = await apiClient.knowledgeSync(documents);
14596
+ uploadSpinner.stop();
14597
+ if (!result.success) {
14598
+ error(`Knowledge sync failed: ${result.error}`);
14599
+ jsonOutput({ success: false, error: result.error });
14600
+ process.exit(1);
14601
+ return;
14602
+ }
14603
+ success(
14604
+ `Knowledge sync complete: ${result.uploaded} uploaded, ${result.skipped} skipped, ${result.errors} errors`
14605
+ );
14606
+ if (options.verbose && result.details) {
14607
+ for (const detail of result.details) {
14608
+ if (detail.status === "uploaded") {
14609
+ info(` + ${detail.filename}`);
14610
+ } else if (detail.status === "skipped") {
14611
+ info(` = ${detail.filename} (unchanged)`);
14612
+ } else {
14613
+ warn(` ! ${detail.filename}: ${detail.error}`);
14614
+ }
14615
+ }
14616
+ }
14617
+ jsonOutput({
14618
+ success: true,
14619
+ uploaded: result.uploaded,
14620
+ skipped: result.skipped,
14621
+ errors: result.errors,
14622
+ details: result.details
14623
+ });
14624
+ } catch (err) {
14625
+ error(`Knowledge sync failed: ${stringifyUnknown(err)}`);
14626
+ jsonOutput({ success: false, error: stringifyUnknown(err) });
14627
+ if (options.verbose) {
14628
+ console.error(err);
14629
+ }
14630
+ process.exit(1);
14631
+ }
14632
+ });
14633
+ return cmd;
14634
+ }
14635
+
12676
14636
  // src/cli/index.ts
12677
14637
  async function main() {
12678
- const program2 = new import_commander8.Command();
14638
+ const program2 = new import_commander9.Command();
12679
14639
  program2.name("appilots").description(
12680
14640
  "Analyze your React Native app and generate MCP documents for AI-powered navigation.\n\nQuick start:\n $ appilots init --api-key ak_xxx\n $ appilots sync\n $ appilots watch"
12681
14641
  ).version(CLI_VERSION).option("--verbose", "Enable verbose logging");
@@ -12686,6 +14646,7 @@ async function main() {
12686
14646
  program2.addCommand(annotateCommand());
12687
14647
  program2.addCommand(statusCommand());
12688
14648
  program2.addCommand(evalCommand());
14649
+ program2.addCommand(knowledgeCommand());
12689
14650
  const args = process.argv.slice(2);
12690
14651
  const firstArg = args[0];
12691
14652
  const hasCommand = firstArg !== void 0 && args.length > 0 && !firstArg.startsWith("-");