@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/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  var fs = require('fs/promises');
4
- var path8 = require('path');
5
- var traverse4 = require('@babel/traverse');
4
+ var crypto = require('crypto');
5
+ var traverse5 = require('@babel/traverse');
6
6
  var BabelTypes = require('@babel/types');
7
- var glob = require('fast-glob');
7
+ var path2 = require('path');
8
+ var fastGlob = require('fast-glob');
8
9
  var parser = require('@babel/parser');
9
10
  var fs$1 = require('fs');
10
- var crypto = require('crypto');
11
11
 
12
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
 
@@ -30,10 +30,10 @@ function _interopNamespace(e) {
30
30
  }
31
31
 
32
32
  var fs__default = /*#__PURE__*/_interopDefault(fs);
33
- var path8__namespace = /*#__PURE__*/_interopNamespace(path8);
34
- var traverse4__default = /*#__PURE__*/_interopDefault(traverse4);
33
+ var traverse5__default = /*#__PURE__*/_interopDefault(traverse5);
35
34
  var BabelTypes__namespace = /*#__PURE__*/_interopNamespace(BabelTypes);
36
- var glob__default = /*#__PURE__*/_interopDefault(glob);
35
+ var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
36
+ var fastGlob__default = /*#__PURE__*/_interopDefault(fastGlob);
37
37
  var parser__namespace = /*#__PURE__*/_interopNamespace(parser);
38
38
 
39
39
  var __defProp = Object.defineProperty;
@@ -41,6 +41,350 @@ var __export = (target, all) => {
41
41
  for (var name in all)
42
42
  __defProp(target, name, { get: all[name], enumerable: true });
43
43
  };
44
+ var MAX_HOPS = 8;
45
+ function unwrap(path11) {
46
+ while (path11.isTSAsExpression() || path11.isTSTypeAssertion() || path11.isTSNonNullExpression() || path11.isTSSatisfiesExpression() || path11.isParenthesizedExpression())
47
+ path11 = path11.get("expression");
48
+ return path11;
49
+ }
50
+ function constantValue(path11, depth = 0) {
51
+ if (depth > MAX_HOPS) return void 0;
52
+ path11 = unwrap(path11);
53
+ if (!path11.isIdentifier()) return path11;
54
+ const binding = path11.scope.getBinding(path11.node.name);
55
+ if (!binding?.constant || !binding.path.isVariableDeclarator()) return path11;
56
+ const init = binding.path.get("init");
57
+ const resolved = init.node ? constantValue(init, depth + 1) : void 0;
58
+ if (resolved?.isObjectExpression() && binding.referencePaths.some(
59
+ (reference) => !reference.parentPath?.isJSXSpreadAttribute() && !reference.parentPath?.isSpreadElement()
60
+ ))
61
+ return void 0;
62
+ return resolved;
63
+ }
64
+ function propValue(path11, name) {
65
+ const readObject = (path12) => {
66
+ const resolved = constantValue(path12);
67
+ if (!resolved?.isObjectExpression()) return { blocked: true };
68
+ for (const property of [...resolved.get("properties")].reverse()) {
69
+ if (property.isSpreadElement()) {
70
+ const found = readObjectBounded(property.get("argument"));
71
+ if (found.value || found.blocked) return found;
72
+ } else if (property.isObjectProperty() || property.isObjectMethod()) {
73
+ if (property.node.computed) return { blocked: true };
74
+ const key = property.node.key;
75
+ if ((BabelTypes__namespace.isIdentifier(key) ? key.name : BabelTypes__namespace.isStringLiteral(key) ? key.value : "") === name)
76
+ return property.isObjectProperty() ? { value: property.get("value") } : { value: property };
77
+ }
78
+ }
79
+ return {};
80
+ };
81
+ let objectBudget = MAX_HOPS;
82
+ const readObjectBounded = (path12) => objectBudget-- > 0 ? readObject(path12) : { blocked: true };
83
+ for (const attr of [...path11.get("attributes")].reverse()) {
84
+ if (attr.isJSXAttribute() && BabelTypes__namespace.isJSXIdentifier(attr.node.name, { name })) {
85
+ const value = attr.get("value");
86
+ return value.isJSXExpressionContainer() ? unwrap(value.get("expression")) : value.node ? value : void 0;
87
+ }
88
+ if (attr.isJSXSpreadAttribute()) {
89
+ const found = readObjectBounded(attr.get("argument"));
90
+ if (found.value || found.blocked) return found.value;
91
+ }
92
+ }
93
+ return void 0;
94
+ }
95
+ function importIdentity(path11, depth = 0) {
96
+ if (depth > MAX_HOPS) return void 0;
97
+ path11 = unwrap(path11);
98
+ if (path11.isIdentifier() || path11.isJSXIdentifier()) {
99
+ const binding = path11.scope.getBinding(path11.node.name);
100
+ if (!binding?.constant) return void 0;
101
+ const declaration = binding.path;
102
+ if (declaration.parentPath?.isImportDeclaration()) {
103
+ const module = declaration.parentPath.node.source.value;
104
+ if (declaration.isImportSpecifier()) {
105
+ const imported = declaration.node.imported;
106
+ return { module, imported: BabelTypes__namespace.isIdentifier(imported) ? imported.name : imported.value };
107
+ }
108
+ if (declaration.isImportDefaultSpecifier()) return { module, imported: "default" };
109
+ if (declaration.isImportNamespaceSpecifier()) return { module, imported: "*" };
110
+ }
111
+ if (declaration.isVariableDeclarator() && declaration.get("init").node)
112
+ return importIdentity(declaration.get("init"), depth + 1);
113
+ }
114
+ if (path11.isJSXMemberExpression() || path11.isMemberExpression() && !path11.node.computed) {
115
+ const origin = importIdentity(path11.get("object"), depth + 1);
116
+ const key = path11.node.property;
117
+ if (origin && (BabelTypes__namespace.isIdentifier(key) || BabelTypes__namespace.isJSXIdentifier(key)) && (origin.imported === "*" || origin.module === "react" && origin.imported === "default"))
118
+ return { module: origin.module, imported: key.name };
119
+ }
120
+ return void 0;
121
+ }
122
+ function handlerFunction(path11, depth = 0) {
123
+ if (depth > MAX_HOPS) return void 0;
124
+ path11 = unwrap(path11);
125
+ if (path11.isFunction()) return path11;
126
+ if (path11.isIdentifier()) {
127
+ const binding = path11.scope.getBinding(path11.node.name);
128
+ if (!binding?.constant) return void 0;
129
+ if (binding.path.isFunctionDeclaration()) return binding.path;
130
+ if (binding.path.isVariableDeclarator() && binding.path.get("init").node)
131
+ return handlerFunction(binding.path.get("init"), depth + 1);
132
+ }
133
+ if (path11.isCallExpression()) {
134
+ const origin = importIdentity(path11.get("callee"));
135
+ if (origin?.module === "react" && origin.imported === "useCallback") {
136
+ const callback = path11.get("arguments")[0];
137
+ if (callback) return handlerFunction(callback, depth + 1);
138
+ }
139
+ }
140
+ if (path11.isMemberExpression() && !path11.node.computed && BabelTypes__namespace.isThisExpression(path11.node.object)) {
141
+ const key = path11.node.property;
142
+ if (!BabelTypes__namespace.isIdentifier(key)) return void 0;
143
+ const owner = path11.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
144
+ if (!owner || !(owner.isClassDeclaration() || owner.isClassExpression())) return void 0;
145
+ for (const member of owner.get("body").get("body")) {
146
+ if (!(member.isClassMethod() || member.isClassProperty()) || member.node.computed || member.node.static)
147
+ continue;
148
+ if (!BabelTypes__namespace.isIdentifier(member.node.key, { name: key.name })) continue;
149
+ if (member.isClassMethod() && member.node.kind === "method") return member;
150
+ if (member.isClassProperty() && member.get("value").node)
151
+ return handlerFunction(member.get("value"), depth + 1);
152
+ }
153
+ }
154
+ return void 0;
155
+ }
156
+
157
+ // src/extractors/control-evidence.ts
158
+ function symbol(node) {
159
+ if (BabelTypes__namespace.isTSAsExpression(node) || BabelTypes__namespace.isTSTypeAssertion(node) || BabelTypes__namespace.isTSNonNullExpression(node) || BabelTypes__namespace.isTSSatisfiesExpression(node))
160
+ return symbol(node.expression);
161
+ if (BabelTypes__namespace.isIdentifier(node) || BabelTypes__namespace.isJSXIdentifier(node)) return node.name;
162
+ if (BabelTypes__namespace.isThisExpression(node)) return "this";
163
+ if (BabelTypes__namespace.isMemberExpression(node) && !node.computed || BabelTypes__namespace.isJSXMemberExpression(node)) {
164
+ const object = symbol(node.object), property = symbol(node.property);
165
+ return object && property ? `${object}.${property}` : void 0;
166
+ }
167
+ if (BabelTypes__namespace.isUnaryExpression(node) && node.operator === "!") {
168
+ const value = symbol(node.argument);
169
+ return value ? `!${value}` : void 0;
170
+ }
171
+ return void 0;
172
+ }
173
+ function attribute(node, name) {
174
+ return node.attributes.find(
175
+ (a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name, { name })
176
+ );
177
+ }
178
+ var ICON_LIBRARIES = [
179
+ "lucide-react-native",
180
+ "lucide-react",
181
+ "@tamagui/lucide-icons",
182
+ "@expo/vector-icons",
183
+ "@react-native-vector-icons/",
184
+ "react-native-vector-icons/"
185
+ ];
186
+ function iconLibrary(module) {
187
+ return ICON_LIBRARIES.some(
188
+ (name) => name.endsWith("/") ? module.startsWith(name) : module === name || module.startsWith(name + "/")
189
+ );
190
+ }
191
+ function add(values, value, max = 12) {
192
+ if (value && value.length <= 240 && values.length < max && !values.includes(value))
193
+ values.push(value);
194
+ }
195
+ function iconName(path11, explicitIcon = false) {
196
+ const origin = importIdentity(path11);
197
+ if (origin && iconLibrary(origin.module) && origin.imported !== "*")
198
+ return origin.imported === "default" ? origin.module : `${origin.module}:${origin.imported}`;
199
+ const name = symbol(path11.node);
200
+ return name && (explicitIcon || /icon/i.test(name)) ? name : void 0;
201
+ }
202
+ function iconsInElement(path11, explicitIcon = false) {
203
+ const name = iconName(path11.get("name"), explicitIcon);
204
+ if (!name) return void 0;
205
+ const glyph = propValue(path11, "name");
206
+ const value = glyph && constantValue(glyph);
207
+ return value?.isStringLiteral() ? `${name}:${value.node.value}` : name;
208
+ }
209
+ function hasInteraction(path11) {
210
+ return ["onPress", "onLongPress", "onClick"].some(
211
+ (name) => attribute(path11.node, name) || propValue(path11, name)
212
+ );
213
+ }
214
+ function collectPresentationIcons(path11, icons) {
215
+ const namedIconProps = [
216
+ "icon",
217
+ "prefix",
218
+ "suffix",
219
+ "left",
220
+ "right",
221
+ "leadingIcon",
222
+ "trailingIcon",
223
+ "startIcon",
224
+ "endIcon",
225
+ "renderIcon"
226
+ ];
227
+ const props = new Set(namedIconProps);
228
+ for (const attr of path11.node.attributes)
229
+ if (BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && !/^on[A-Z]/.test(attr.name.name))
230
+ props.add(attr.name.name);
231
+ for (const prop of props) {
232
+ const icon = propValue(path11, prop);
233
+ if (icon) {
234
+ const value = constantValue(icon);
235
+ if (value?.isStringLiteral()) {
236
+ if (prop.toLowerCase().includes("icon")) add(icons, value.node.value, 8);
237
+ } else {
238
+ const explicitIcon = prop.toLowerCase() === "icon";
239
+ if (icon.isJSXElement()) {
240
+ if (hasInteraction(icon.get("openingElement"))) continue;
241
+ add(icons, iconsInElement(icon.get("openingElement"), explicitIcon), 8);
242
+ }
243
+ icon.traverse({
244
+ JSXAttribute(attr) {
245
+ if (BabelTypes__namespace.isJSXIdentifier(attr.node.name) && /^on[A-Z]/.test(attr.node.name.name))
246
+ attr.skip();
247
+ },
248
+ JSXElement(child) {
249
+ const opening = child.get("openingElement");
250
+ if (hasInteraction(opening)) {
251
+ child.skip();
252
+ return;
253
+ }
254
+ add(icons, iconsInElement(opening, explicitIcon), 8);
255
+ }
256
+ });
257
+ if (!icons.length && (namedIconProps.includes(prop) || explicitIcon))
258
+ add(icons, iconName(icon, explicitIcon), 8);
259
+ }
260
+ }
261
+ }
262
+ }
263
+ function collectIcons(path11) {
264
+ const icons = [];
265
+ collectPresentationIcons(path11, icons);
266
+ const jsx = path11.parentPath;
267
+ if (jsx.isJSXElement())
268
+ jsx.traverse({
269
+ // JSX mentioned inside an event callback is not a rendered child icon.
270
+ JSXAttribute(attr) {
271
+ attr.skip();
272
+ },
273
+ JSXElement(child) {
274
+ const opening = child.get("openingElement");
275
+ if (hasInteraction(opening)) {
276
+ child.skip();
277
+ return;
278
+ }
279
+ add(icons, iconsInElement(opening), 8);
280
+ collectPresentationIcons(opening, icons);
281
+ }
282
+ });
283
+ return icons;
284
+ }
285
+ function conditionsAt(path11) {
286
+ const conditions = [];
287
+ let child = path11;
288
+ for (let parent = child.parentPath; parent && !parent.isFunction(); child = parent, parent = parent.parentPath) {
289
+ let test;
290
+ let negated = false;
291
+ if (parent.isLogicalExpression() && parent.node.right === child.node) {
292
+ if (parent.node.operator === "&&") test = parent.node.left;
293
+ if (parent.node.operator === "||") {
294
+ test = parent.node.left;
295
+ negated = true;
296
+ }
297
+ } else if (parent.isConditionalExpression() && parent.node.test !== child.node) {
298
+ test = parent.node.test;
299
+ negated = parent.node.alternate === child.node;
300
+ } else if (parent.isIfStatement() && parent.node.test !== child.node) {
301
+ test = parent.node.test;
302
+ negated = parent.node.alternate === child.node;
303
+ }
304
+ const name = symbol(test);
305
+ if (name) add(conditions, negated ? name.startsWith("!") ? name.slice(1) : `!${name}` : name);
306
+ }
307
+ return conditions;
308
+ }
309
+ function collectHandlerEvidence(expression, evidence) {
310
+ const seen = /* @__PURE__ */ new Set();
311
+ let budget = 100;
312
+ const visit = (path11, depth) => {
313
+ if (depth > 4 || budget-- <= 0) return;
314
+ const fn = handlerFunction(path11);
315
+ if (!fn || seen.has(fn.node)) return;
316
+ seen.add(fn.node);
317
+ fn.traverse({
318
+ // Ignore uncalled helper definitions; inline callbacks remain source evidence.
319
+ Function(nested) {
320
+ if (!nested.parentPath.isCallExpression() && !nested.parentPath.isObjectProperty())
321
+ nested.skip();
322
+ },
323
+ CallExpression(call) {
324
+ if (budget-- <= 0) {
325
+ call.skip();
326
+ return;
327
+ }
328
+ add(evidence.calls, symbol(call.node.callee));
329
+ for (const arg of call.node.arguments) add(evidence.argumentBindings, symbol(arg));
330
+ visit(call.get("callee"), depth + 1);
331
+ if (!BabelTypes__namespace.isMemberExpression(call.node.callee) || call.node.callee.computed || !BabelTypes__namespace.isIdentifier(call.node.callee.property, { name: "alert" }))
332
+ return;
333
+ const callee = call.get("callee");
334
+ if (!callee.isMemberExpression()) return;
335
+ const origin = importIdentity(callee.get("object"));
336
+ if (origin?.module !== "react-native" || origin.imported !== "Alert") return;
337
+ const options = call.node.arguments[2];
338
+ const destructiveOption = BabelTypes__namespace.isArrayExpression(options) && options.elements.some(
339
+ (option) => BabelTypes__namespace.isObjectExpression(option) && option.properties.some(
340
+ (p) => BabelTypes__namespace.isObjectProperty(p) && !p.computed && symbol(p.key) === "style" && BabelTypes__namespace.isStringLiteral(p.value, { value: "destructive" })
341
+ )
342
+ );
343
+ if (destructiveOption)
344
+ evidence.nativeConfirmation = {
345
+ title: symbol(call.node.arguments[0]),
346
+ destructiveOption
347
+ };
348
+ }
349
+ });
350
+ };
351
+ visit(unwrap(expression), 0);
352
+ }
353
+ function extractControlEvidence(ast, source, file) {
354
+ const candidates = [];
355
+ const sourceHash = crypto.createHash("sha256").update(source).digest("hex");
356
+ traverse5__default.default(ast, {
357
+ JSXOpeningElement(path11) {
358
+ const node = path11.node;
359
+ if (attribute(node, "__appilotsControl")) return;
360
+ const value = propValue(path11, "onPress");
361
+ if (!value && !attribute(node, "onPress") || value?.isNullLiteral() || value?.isJSXEmptyExpression())
362
+ return;
363
+ const icons = collectIcons(path11);
364
+ if (!icons.length || node.end == null) return;
365
+ const evidence = {
366
+ version: 1,
367
+ siteId: crypto.createHash("sha256").update(`${file}:${sourceHash}:${node.start}`).digest("hex").slice(0, 20),
368
+ component: symbol(node.name) ?? "unknown",
369
+ icons,
370
+ ...value && symbol(value.node) ? { handler: symbol(value.node) } : {},
371
+ calls: [],
372
+ argumentBindings: [],
373
+ conditions: conditionsAt(path11)
374
+ };
375
+ if (value) collectHandlerEvidence(value, evidence);
376
+ candidates.push({ evidence, sourceHash, offset: node.end - (node.selfClosing ? 2 : 1) });
377
+ }
378
+ });
379
+ return candidates;
380
+ }
381
+ function byCodeUnit(a, b) {
382
+ return a < b ? -1 : a > b ? 1 : 0;
383
+ }
384
+ async function globSorted(patterns, options) {
385
+ const files = await fastGlob__default.default(patterns, options);
386
+ return files.sort(byCodeUnit);
387
+ }
44
388
  var DEFAULT_PARSER_PLUGINS = [
45
389
  "jsx",
46
390
  "typescript",
@@ -155,7 +499,7 @@ function classifyJsxComponent(name, element) {
155
499
  }
156
500
  function collectFunctions(ast) {
157
501
  const handlers = /* @__PURE__ */ new Map();
158
- traverse4__default.default(ast, {
502
+ traverse5__default.default(ast, {
159
503
  FunctionDeclaration: (nodePath) => {
160
504
  if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
161
505
  },
@@ -258,7 +602,7 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
258
602
  }
259
603
  };
260
604
  if (fn.body) {
261
- traverse4__default.default(fn.body, {
605
+ traverse5__default.default(fn.body, {
262
606
  noScope: true,
263
607
  enter: (nodePath) => inspectNode(nodePath.node)
264
608
  });
@@ -286,7 +630,7 @@ function setterToStateName(setterName) {
286
630
  }
287
631
  function extractNavigationCalls(ast) {
288
632
  const calls = [];
289
- traverse4__default.default(ast, {
633
+ traverse5__default.default(ast, {
290
634
  noScope: !BabelTypes__namespace.isFile(ast),
291
635
  CallExpression: (nodePath) => {
292
636
  const node = nodePath.node;
@@ -425,14 +769,26 @@ var ScreenAnalyzer = class {
425
769
  strictScreens;
426
770
  /** Glob patterns that identify screen files in strict mode */
427
771
  screenPatterns;
772
+ /**
773
+ * Arquivos que uma ROTA monta (`component={…}` resolvido pelo
774
+ * `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
775
+ * convenção de nome, porque um componente que uma rota monta É uma tela por
776
+ * definição — não é heurística, é o que o app declarou.
777
+ *
778
+ * É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
779
+ * em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
780
+ */
781
+ routeTargetFiles;
428
782
  /** §D: Count of screens filtered out in strict mode (available after analyze()) */
429
783
  screensFilteredOut = 0;
784
+ controlEvidenceFiles = {};
430
785
  constructor(config, options) {
431
786
  this.config = config;
787
+ this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
432
788
  this.strictScreens = options?.strictScreens ?? false;
433
789
  this.screenPatterns = options?.screenPatterns ?? [
434
- "**/*Screen.{ts,tsx}",
435
- "**/screens/**/*.{ts,tsx}"
790
+ "**/*Screen.{ts,tsx,js,jsx}",
791
+ "**/screens/**/*.{ts,tsx,js,jsx}"
436
792
  ];
437
793
  }
438
794
  /** Analyze all screens in the project */
@@ -444,7 +800,7 @@ var ScreenAnalyzer = class {
444
800
  console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
445
801
  }
446
802
  }
447
- const files = await glob__default.default(include, {
803
+ const files = await globSorted(include, {
448
804
  cwd: this.config.rootDir,
449
805
  ignore: exclude
450
806
  });
@@ -453,23 +809,24 @@ var ScreenAnalyzer = class {
453
809
  }
454
810
  let screenPatternFiles = null;
455
811
  if (this.strictScreens) {
456
- const matched = await glob__default.default(this.screenPatterns, {
812
+ const matched = await globSorted(this.screenPatterns, {
457
813
  cwd: this.config.rootDir,
458
814
  ignore: exclude
459
815
  });
460
- screenPatternFiles = new Set(matched.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
816
+ screenPatternFiles = new Set(matched.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
461
817
  }
462
818
  const screens = [];
463
819
  this.screensFilteredOut = 0;
464
820
  for (const file of files) {
465
- const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
821
+ const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
466
822
  try {
467
823
  const descriptor = await this.analyzeFile(filePath);
468
824
  if (!descriptor) continue;
469
825
  if (this.strictScreens) {
470
826
  const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
471
827
  const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
472
- if (!hasRegisterScreen && !matchesPattern) {
828
+ const isRouteTarget = this.routeTargetFiles.has(filePath);
829
+ if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
473
830
  this.screensFilteredOut++;
474
831
  if (this.verbose) {
475
832
  console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
@@ -525,6 +882,11 @@ var ScreenAnalyzer = class {
525
882
  title: registerScreenMeta?.title,
526
883
  description: registerScreenMeta?.description,
527
884
  components,
885
+ controlCandidates: extractControlEvidence(
886
+ ast,
887
+ source,
888
+ path2__namespace.default.relative(this.config.rootDir, filePath)
889
+ ),
528
890
  forms,
529
891
  actions,
530
892
  navigationTargets,
@@ -535,6 +897,8 @@ var ScreenAnalyzer = class {
535
897
  ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
536
898
  } : {}
537
899
  };
900
+ if (descriptor.controlCandidates?.length)
901
+ this.controlEvidenceFiles[path2__namespace.default.relative(this.config.rootDir, filePath).split(path2__namespace.default.sep).join("/")] = descriptor.controlCandidates;
538
902
  descriptor.__hasRegisterScreen = hasRegisterScreenCall;
539
903
  return descriptor;
540
904
  }
@@ -544,7 +908,7 @@ var ScreenAnalyzer = class {
544
908
  */
545
909
  detectRegisterScreenCall(ast) {
546
910
  let found = false;
547
- traverse4__default.default(ast, {
911
+ traverse5__default.default(ast, {
548
912
  CallExpression: (nodePath) => {
549
913
  if (found) return;
550
914
  const callee = nodePath.node.callee;
@@ -561,7 +925,7 @@ var ScreenAnalyzer = class {
561
925
  */
562
926
  extractRegisterScreenMetadata(ast) {
563
927
  let metadata = null;
564
- traverse4__default.default(ast, {
928
+ traverse5__default.default(ast, {
565
929
  CallExpression: (nodePath) => {
566
930
  const callee = nodePath.node.callee;
567
931
  if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
@@ -677,6 +1041,8 @@ var ScreenAnalyzer = class {
677
1041
  action.riskLevel = value.value;
678
1042
  } else if (key === "nativeConfirmationExpected" && BabelTypes__namespace.isBooleanLiteral(value)) {
679
1043
  action.nativeConfirmationExpected = value.value;
1044
+ } else if (key === "asyncBudgetMs" && BabelTypes__namespace.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
1045
+ action.asyncBudgetMs = value.value;
680
1046
  } else if (key === "appilotsInferred" && BabelTypes__namespace.isObjectExpression(value)) {
681
1047
  action.appilotsInferred = this.parseAppilotsInferredObject(value);
682
1048
  }
@@ -856,7 +1222,7 @@ var ScreenAnalyzer = class {
856
1222
  */
857
1223
  extractDefaultComponentName(ast) {
858
1224
  let componentName = "";
859
- traverse4__default.default(ast, {
1225
+ traverse5__default.default(ast, {
860
1226
  ExportDefaultDeclaration: (nodePath) => {
861
1227
  const declaration = nodePath.node.declaration;
862
1228
  if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id?.name) {
@@ -881,7 +1247,7 @@ var ScreenAnalyzer = class {
881
1247
  */
882
1248
  extractNavigationTargets(ast) {
883
1249
  const targets = /* @__PURE__ */ new Set();
884
- traverse4__default.default(ast, {
1250
+ traverse5__default.default(ast, {
885
1251
  CallExpression: (nodePath) => {
886
1252
  const callee = nodePath.node.callee;
887
1253
  if (BabelTypes__namespace.isMemberExpression(callee) && BabelTypes__namespace.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes__namespace.isIdentifier(callee.property) && callee.property.name === "navigate") {
@@ -900,7 +1266,7 @@ var ScreenAnalyzer = class {
900
1266
  extractForms(ast) {
901
1267
  const forms = [];
902
1268
  const fields = /* @__PURE__ */ new Map();
903
- traverse4__default.default(ast, {
1269
+ traverse5__default.default(ast, {
904
1270
  JSXOpeningElement: (nodePath) => {
905
1271
  const element = nodePath.node;
906
1272
  if (BabelTypes__namespace.isJSXIdentifier(element.name)) {
@@ -1021,7 +1387,7 @@ var ScreenAnalyzer = class {
1021
1387
  extractComponents(ast) {
1022
1388
  const components = [];
1023
1389
  const seen = /* @__PURE__ */ new Set();
1024
- traverse4__default.default(ast, {
1390
+ traverse5__default.default(ast, {
1025
1391
  JSXOpeningElement: (nodePath) => {
1026
1392
  const element = nodePath.node;
1027
1393
  if (BabelTypes__namespace.isJSXIdentifier(element.name)) {
@@ -1085,7 +1451,7 @@ var ScreenAnalyzer = class {
1085
1451
  const actionLabels = new Map(
1086
1452
  actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
1087
1453
  );
1088
- traverse4__default.default(ast, {
1454
+ traverse5__default.default(ast, {
1089
1455
  JSXOpeningElement: (nodePath) => {
1090
1456
  const element = nodePath.node;
1091
1457
  if (BabelTypes__namespace.isJSXIdentifier(element.name)) {
@@ -1154,7 +1520,7 @@ var ScreenAnalyzer = class {
1154
1520
  }
1155
1521
  collectButtonHandlersByLabel(ast) {
1156
1522
  const out = /* @__PURE__ */ new Map();
1157
- traverse4__default.default(ast, {
1523
+ traverse5__default.default(ast, {
1158
1524
  JSXOpeningElement: (nodePath) => {
1159
1525
  const element = nodePath.node;
1160
1526
  if (!BabelTypes__namespace.isJSXIdentifier(element.name)) return;
@@ -1236,7 +1602,7 @@ var ScreenAnalyzer = class {
1236
1602
  }
1237
1603
  };
1238
1604
  if (fn.body) {
1239
- traverse4__default.default(
1605
+ traverse5__default.default(
1240
1606
  fn.body,
1241
1607
  {
1242
1608
  noScope: true,
@@ -1433,7 +1799,7 @@ var ScreenAnalyzer = class {
1433
1799
  extractCollections(ast) {
1434
1800
  const renderItemFns = this.collectRenderItemFunctions(ast);
1435
1801
  const collections = [];
1436
- traverse4__default.default(ast, {
1802
+ traverse5__default.default(ast, {
1437
1803
  JSXOpeningElement: (nodePath) => {
1438
1804
  const element = nodePath.node;
1439
1805
  if (!BabelTypes__namespace.isJSXIdentifier(element.name)) return;
@@ -1470,7 +1836,7 @@ var ScreenAnalyzer = class {
1470
1836
  }
1471
1837
  collectRenderItemFunctions(ast) {
1472
1838
  const out = /* @__PURE__ */ new Map();
1473
- traverse4__default.default(ast, {
1839
+ traverse5__default.default(ast, {
1474
1840
  VariableDeclarator: (nodePath) => {
1475
1841
  if (!BabelTypes__namespace.isIdentifier(nodePath.node.id)) return;
1476
1842
  const init = nodePath.node.init;
@@ -1513,7 +1879,7 @@ var ScreenAnalyzer = class {
1513
1879
  }
1514
1880
  extractRowAction(fn) {
1515
1881
  let action;
1516
- traverse4__default.default(
1882
+ traverse5__default.default(
1517
1883
  fn.body,
1518
1884
  {
1519
1885
  noScope: true,
@@ -1566,7 +1932,7 @@ var ScreenAnalyzer = class {
1566
1932
  } else if (BabelTypes__namespace.isIdentifier(firstParam)) {
1567
1933
  itemNames.add(firstParam.name);
1568
1934
  }
1569
- traverse4__default.default(
1935
+ traverse5__default.default(
1570
1936
  fn.body,
1571
1937
  {
1572
1938
  noScope: true,
@@ -1608,7 +1974,7 @@ var ScreenAnalyzer = class {
1608
1974
  inferSearchField(ast, dataSource) {
1609
1975
  if (!dataSource) return void 0;
1610
1976
  let queryBinding;
1611
- traverse4__default.default(ast, {
1977
+ traverse5__default.default(ast, {
1612
1978
  CallExpression: (nodePath) => {
1613
1979
  const node = nodePath.node;
1614
1980
  if (!BabelTypes__namespace.isMemberExpression(node.callee)) return;
@@ -1619,7 +1985,7 @@ var ScreenAnalyzer = class {
1619
1985
  const fn = node.arguments[0];
1620
1986
  if (!BabelTypes__namespace.isArrowFunctionExpression(fn) && !BabelTypes__namespace.isFunctionExpression(fn))
1621
1987
  return;
1622
- traverse4__default.default(
1988
+ traverse5__default.default(
1623
1989
  fn.body,
1624
1990
  {
1625
1991
  noScope: true,
@@ -1656,43 +2022,871 @@ var ScreenAnalyzer = class {
1656
2022
  * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1657
2023
  */
1658
2024
  extractScreenName(filePath) {
1659
- const basename2 = path8__namespace.default.basename(filePath);
2025
+ const basename2 = path2__namespace.default.basename(filePath);
1660
2026
  return basename2.replace(/\.(tsx?|jsx?)$/, "");
1661
2027
  }
1662
2028
  };
2029
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
2030
+ var MAX_HOPS2 = 8;
2031
+ var ModuleGraph = class {
2032
+ asts = /* @__PURE__ */ new Map();
2033
+ resolved = /* @__PURE__ */ new Map();
2034
+ /** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
2035
+ aliases;
2036
+ /** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
2037
+ workspacePackages;
2038
+ constructor(rootDir) {
2039
+ this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
2040
+ this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
2041
+ }
2042
+ /** AST de um arquivo, memoizada. `null` quando não parseia. */
2043
+ parse(file) {
2044
+ const cached = this.asts.get(file);
2045
+ if (cached !== void 0) return cached;
2046
+ let ast = null;
2047
+ try {
2048
+ ast = parser__namespace.parse(fs$1.readFileSync(file, "utf-8"), {
2049
+ sourceType: "module",
2050
+ plugins: ["jsx", "typescript"]
2051
+ });
2052
+ } catch {
2053
+ ast = null;
2054
+ }
2055
+ this.asts.set(file, ast);
2056
+ return ast;
2057
+ }
2058
+ /**
2059
+ * `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
2060
+ * Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
2061
+ * é de terceiro e não tem tela nossa dentro.
2062
+ */
2063
+ resolve(fromFile, spec) {
2064
+ const key = `${fromFile} ${spec}`;
2065
+ const cached = this.resolved.get(key);
2066
+ if (cached !== void 0) return cached;
2067
+ let base = null;
2068
+ if (spec.startsWith(".")) {
2069
+ base = path2__namespace.default.resolve(path2__namespace.default.dirname(fromFile), spec);
2070
+ } else {
2071
+ for (const { prefix, target } of this.aliases) {
2072
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
2073
+ base = path2__namespace.default.join(target, spec.slice(prefix.length));
2074
+ break;
2075
+ }
2076
+ }
2077
+ if (!base) {
2078
+ for (const pkg of this.workspacePackages) {
2079
+ if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
2080
+ base = path2__namespace.default.join(pkg.dir, spec.slice(pkg.name.length));
2081
+ break;
2082
+ }
2083
+ }
2084
+ }
2085
+ }
2086
+ if (!base) {
2087
+ this.resolved.set(key, null);
2088
+ return null;
2089
+ }
2090
+ const candidates = [
2091
+ base,
2092
+ ...EXTENSIONS.map((e) => base + e),
2093
+ ...EXTENSIONS.map((e) => path2__namespace.default.join(base, "index" + e))
2094
+ ];
2095
+ let found = null;
2096
+ for (const c of candidates) {
2097
+ try {
2098
+ if (fs$1.existsSync(c) && fs$1.statSync(c).isFile()) {
2099
+ found = c;
2100
+ break;
2101
+ }
2102
+ } catch {
2103
+ }
2104
+ }
2105
+ this.resolved.set(key, found);
2106
+ return found;
2107
+ }
2108
+ /** `import`s do arquivo, por nome local. */
2109
+ imports(file) {
2110
+ const out = /* @__PURE__ */ new Map();
2111
+ const ast = this.parse(file);
2112
+ if (!ast) return out;
2113
+ for (const stmt of ast.program.body) {
2114
+ if (!BabelTypes__namespace.isImportDeclaration(stmt)) continue;
2115
+ const source = stmt.source.value;
2116
+ for (const spec of stmt.specifiers) {
2117
+ if (BabelTypes__namespace.isImportDefaultSpecifier(spec)) {
2118
+ out.set(spec.local.name, { source, imported: "default" });
2119
+ } else if (BabelTypes__namespace.isImportNamespaceSpecifier(spec)) {
2120
+ out.set(spec.local.name, { source, imported: "*" });
2121
+ } else if (BabelTypes__namespace.isImportSpecifier(spec)) {
2122
+ const imported = BabelTypes__namespace.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
2123
+ out.set(spec.local.name, { source, imported });
2124
+ }
2125
+ }
2126
+ }
2127
+ return out;
2128
+ }
2129
+ /** `const X = <init>` no topo do arquivo, incluindo `export const`. */
2130
+ topLevelInit(file, name) {
2131
+ const ast = this.parse(file);
2132
+ if (!ast) return null;
2133
+ for (const stmt of ast.program.body) {
2134
+ const decl = BabelTypes__namespace.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
2135
+ if (!BabelTypes__namespace.isVariableDeclaration(decl)) continue;
2136
+ for (const d of decl.declarations) {
2137
+ if (BabelTypes__namespace.isIdentifier(d.id) && d.id.name === name && d.init) {
2138
+ return BabelTypes__namespace.isTSAsExpression(d.init) ? d.init.expression : d.init;
2139
+ }
2140
+ }
2141
+ }
2142
+ return null;
2143
+ }
2144
+ /**
2145
+ * Onde `name` é DEFINIDO — segue import e reexport de barrel.
2146
+ * Devolve o arquivo e o nome sob o qual ele é definido lá.
2147
+ */
2148
+ resolveBinding(file, name, hops = 0) {
2149
+ if (hops > MAX_HOPS2) return null;
2150
+ if (this.topLevelInit(file, name) !== null) return { file, name };
2151
+ const binding = this.imports(file).get(name);
2152
+ if (binding) {
2153
+ const target = this.resolve(file, binding.source);
2154
+ if (!target) return null;
2155
+ const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
2156
+ const deeper = this.resolveBinding(target, next, hops + 1);
2157
+ return deeper ?? { file: target, name: next };
2158
+ }
2159
+ const ast = this.parse(file);
2160
+ if (ast) {
2161
+ for (const stmt of ast.program.body) {
2162
+ if (!BabelTypes__namespace.isExportNamedDeclaration(stmt) || !stmt.source) continue;
2163
+ for (const spec of stmt.specifiers) {
2164
+ if (!BabelTypes__namespace.isExportSpecifier(spec)) continue;
2165
+ const exported = BabelTypes__namespace.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
2166
+ if (exported !== name) continue;
2167
+ const target = this.resolve(file, stmt.source.value);
2168
+ if (!target) return null;
2169
+ const local = spec.local.name;
2170
+ return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
2171
+ }
2172
+ }
2173
+ }
2174
+ if (ast) {
2175
+ for (const stmt of ast.program.body) {
2176
+ if (!BabelTypes__namespace.isExportAllDeclaration(stmt)) continue;
2177
+ const target = this.resolve(file, stmt.source.value);
2178
+ if (!target || target === file) continue;
2179
+ const deeper = this.resolveBinding(target, name, hops + 1);
2180
+ if (deeper) return deeper;
2181
+ }
2182
+ }
2183
+ if (ast) {
2184
+ for (const stmt of ast.program.body) {
2185
+ if (!BabelTypes__namespace.isExportDefaultDeclaration(stmt)) continue;
2186
+ if (BabelTypes__namespace.isIdentifier(stmt.declaration)) {
2187
+ const local = stmt.declaration.name;
2188
+ if (local === name) return null;
2189
+ return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
2190
+ }
2191
+ }
2192
+ }
2193
+ return null;
2194
+ }
2195
+ /**
2196
+ * O valor string de uma expressão de nome de rota, ou `null`.
2197
+ *
2198
+ * Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
2199
+ * seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
2200
+ * com interpolação nem valor calculado, de propósito.
2201
+ */
2202
+ stringConstant(file, node, hops = 0) {
2203
+ if (!node || hops > MAX_HOPS2) return null;
2204
+ if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
2205
+ if (BabelTypes__namespace.isTemplateLiteral(node)) {
2206
+ return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
2207
+ }
2208
+ if (BabelTypes__namespace.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
2209
+ const chain = memberChain(node);
2210
+ if (!chain) return null;
2211
+ const origin = this.resolveBinding(file, chain.root);
2212
+ if (!origin) return null;
2213
+ let current = this.topLevelInit(origin.file, origin.name);
2214
+ if (!current) return null;
2215
+ for (const key of chain.path) {
2216
+ if (!BabelTypes__namespace.isObjectExpression(current)) return null;
2217
+ const prop = objectProperty(current, key);
2218
+ if (!prop) return null;
2219
+ current = BabelTypes__namespace.isTSAsExpression(prop) ? prop.expression : prop;
2220
+ }
2221
+ return BabelTypes__namespace.isStringLiteral(current) ? current.value : null;
2222
+ }
2223
+ /**
2224
+ * O ARQUIVO onde vive o componente de uma rota, ou `null`.
2225
+ *
2226
+ * Aceita as três formas que o corpus mostrou: identificador
2227
+ * (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
2228
+ * e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
2229
+ * que é como o `pocketpal` monta todas as telas do Drawer).
2230
+ */
2231
+ componentFile(file, node, hops = 0) {
2232
+ if (!node || hops > MAX_HOPS2) return null;
2233
+ if (BabelTypes__namespace.isCallExpression(node)) {
2234
+ for (const arg of node.arguments) {
2235
+ if (BabelTypes__namespace.isIdentifier(arg) || BabelTypes__namespace.isMemberExpression(arg)) {
2236
+ const inner = this.componentFile(file, arg, hops + 1);
2237
+ if (inner) return inner;
2238
+ }
2239
+ }
2240
+ return null;
2241
+ }
2242
+ if (BabelTypes__namespace.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
2243
+ const chain = memberChain(node);
2244
+ if (!chain) return null;
2245
+ const origin = this.resolveBinding(file, chain.root);
2246
+ if (!origin) return null;
2247
+ if (chain.path.length === 0) return origin.file;
2248
+ const init = this.topLevelInit(origin.file, origin.name);
2249
+ if (init && BabelTypes__namespace.isObjectExpression(init)) {
2250
+ const prop = objectProperty(init, chain.path[0]);
2251
+ if (prop) return this.componentFile(origin.file, prop, hops + 1);
2252
+ }
2253
+ const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
2254
+ return viaExport?.file ?? null;
2255
+ }
2256
+ /**
2257
+ * Os arquivos DO PROJETO que este arquivo renderiza como JSX.
2258
+ *
2259
+ * `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
2260
+ * Import de pacote devolve `null` no `resolve` e fica de fora: componente de
2261
+ * terceiro não tem tela nossa dentro.
2262
+ *
2263
+ * Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
2264
+ * extractor, e um navegador não é conteúdo de tela.
2265
+ */
2266
+ renderedComponentFiles(file) {
2267
+ const ast = this.parse(file);
2268
+ if (!ast) return [];
2269
+ const names = /* @__PURE__ */ new Set();
2270
+ const visit = (node) => {
2271
+ if (!node || typeof node !== "object") return;
2272
+ if (BabelTypes__namespace.isJSXOpeningElement(node) && BabelTypes__namespace.isJSXIdentifier(node.name)) {
2273
+ const n = node.name.name;
2274
+ if (/^[A-Z]/.test(n)) names.add(n);
2275
+ }
2276
+ for (const key of Object.keys(node)) {
2277
+ const value = node[key];
2278
+ if (Array.isArray(value)) {
2279
+ for (const item of value) visit(item);
2280
+ } else if (value && typeof value === "object" && "type" in value) {
2281
+ visit(value);
2282
+ }
2283
+ }
2284
+ };
2285
+ visit(ast.program);
2286
+ const out = /* @__PURE__ */ new Set();
2287
+ for (const name of names) {
2288
+ const origin = this.resolveBinding(file, name);
2289
+ if (origin && origin.file !== file) out.add(origin.file);
2290
+ }
2291
+ return [...out];
2292
+ }
2293
+ };
2294
+ function memberChain(node) {
2295
+ const chain = [];
2296
+ let current = node;
2297
+ while (BabelTypes__namespace.isMemberExpression(current)) {
2298
+ if (current.computed) {
2299
+ if (!BabelTypes__namespace.isStringLiteral(current.property)) return null;
2300
+ chain.unshift(current.property.value);
2301
+ } else if (BabelTypes__namespace.isIdentifier(current.property)) {
2302
+ chain.unshift(current.property.name);
2303
+ } else {
2304
+ return null;
2305
+ }
2306
+ current = current.object;
2307
+ }
2308
+ return BabelTypes__namespace.isIdentifier(current) ? { root: current.name, path: chain } : null;
2309
+ }
2310
+ function objectProperty(obj, key) {
2311
+ for (const prop of obj.properties) {
2312
+ if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
2313
+ const name = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
2314
+ if (name === key && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
2315
+ }
2316
+ return null;
2317
+ }
2318
+ function readTsconfigAliases(rootDir) {
2319
+ const file = path2__namespace.default.join(rootDir, "tsconfig.json");
2320
+ if (!fs$1.existsSync(file)) return [];
2321
+ let parsed;
2322
+ try {
2323
+ const raw = stripJsonComments(fs$1.readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
2324
+ parsed = JSON.parse(raw);
2325
+ } catch {
2326
+ return [];
2327
+ }
2328
+ const paths = parsed.compilerOptions?.paths;
2329
+ if (!paths) return [];
2330
+ const baseUrl = path2__namespace.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
2331
+ const out = [];
2332
+ for (const [pattern, targets] of Object.entries(paths)) {
2333
+ const first = targets[0];
2334
+ if (typeof first !== "string") continue;
2335
+ out.push({
2336
+ prefix: pattern.replace(/\/?\*$/, ""),
2337
+ target: path2__namespace.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
2338
+ });
2339
+ }
2340
+ return out.sort((a, b) => b.prefix.length - a.prefix.length);
2341
+ }
2342
+ function stripJsonComments(text) {
2343
+ let out = "";
2344
+ let inString = false;
2345
+ let escaped = false;
2346
+ for (let i = 0; i < text.length; i++) {
2347
+ const c = text[i];
2348
+ if (inString) {
2349
+ out += c;
2350
+ if (escaped) escaped = false;
2351
+ else if (c === "\\") escaped = true;
2352
+ else if (c === '"') inString = false;
2353
+ continue;
2354
+ }
2355
+ if (c === '"') {
2356
+ inString = true;
2357
+ out += c;
2358
+ continue;
2359
+ }
2360
+ if (c === "/" && text[i + 1] === "/") {
2361
+ while (i < text.length && text[i] !== "\n") i++;
2362
+ out += "\n";
2363
+ continue;
2364
+ }
2365
+ if (c === "/" && text[i + 1] === "*") {
2366
+ i += 2;
2367
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
2368
+ i++;
2369
+ continue;
2370
+ }
2371
+ out += c;
2372
+ }
2373
+ return out;
2374
+ }
2375
+ function readWorkspacePackages(rootDir) {
2376
+ const start = path2__namespace.default.resolve(rootDir);
2377
+ let dir = start;
2378
+ for (let i = 0; i < 6; i++) {
2379
+ const globs = workspaceGlobs(dir);
2380
+ if (globs.length > 0) {
2381
+ const packages = expandWorkspaceGlobs(dir, globs);
2382
+ const containsApp = packages.some(
2383
+ (p) => start === p.dir || start.startsWith(p.dir + path2__namespace.default.sep)
2384
+ );
2385
+ return containsApp ? packages : [];
2386
+ }
2387
+ const parent = path2__namespace.default.dirname(dir);
2388
+ if (parent === dir) break;
2389
+ dir = parent;
2390
+ }
2391
+ return [];
2392
+ }
2393
+ function workspaceGlobs(dir) {
2394
+ const pnpm = path2__namespace.default.join(dir, "pnpm-workspace.yaml");
2395
+ if (fs$1.existsSync(pnpm)) {
2396
+ try {
2397
+ const lines = fs$1.readFileSync(pnpm, "utf-8").split(/\r?\n/);
2398
+ const out = [];
2399
+ let inPackages = false;
2400
+ for (const line of lines) {
2401
+ if (/^packages:/.test(line)) {
2402
+ inPackages = true;
2403
+ continue;
2404
+ }
2405
+ if (inPackages) {
2406
+ const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
2407
+ if (m) out.push(m[1].trim());
2408
+ else if (/^\S/.test(line)) break;
2409
+ }
2410
+ }
2411
+ if (out.length) return out;
2412
+ } catch {
2413
+ }
2414
+ }
2415
+ const pkgPath = path2__namespace.default.join(dir, "package.json");
2416
+ if (!fs$1.existsSync(pkgPath)) return [];
2417
+ try {
2418
+ const pkg = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8"));
2419
+ const ws = pkg.workspaces;
2420
+ if (Array.isArray(ws)) return ws;
2421
+ if (ws && Array.isArray(ws.packages)) return ws.packages;
2422
+ } catch {
2423
+ }
2424
+ return [];
2425
+ }
2426
+ function expandWorkspaceGlobs(root, globs) {
2427
+ const out = [];
2428
+ const add2 = (dir) => {
2429
+ const pkgPath = path2__namespace.default.join(dir, "package.json");
2430
+ if (!fs$1.existsSync(pkgPath)) return;
2431
+ try {
2432
+ const name = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8")).name;
2433
+ if (name) out.push({ name, dir });
2434
+ } catch {
2435
+ }
2436
+ };
2437
+ for (const glob of globs) {
2438
+ if (glob.endsWith("/*")) {
2439
+ const parent = path2__namespace.default.join(root, glob.slice(0, -2));
2440
+ let entries = [];
2441
+ try {
2442
+ entries = fs$1.readdirSync(parent);
2443
+ } catch {
2444
+ continue;
2445
+ }
2446
+ for (const entry of entries) {
2447
+ const dir = path2__namespace.default.join(parent, entry);
2448
+ try {
2449
+ if (fs$1.statSync(dir).isDirectory()) add2(dir);
2450
+ } catch {
2451
+ }
2452
+ }
2453
+ } else if (!glob.includes("*")) {
2454
+ add2(path2__namespace.default.join(root, glob));
2455
+ }
2456
+ }
2457
+ return out.sort((a, b) => b.name.length - a.name.length);
2458
+ }
2459
+ var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
2460
+ var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
2461
+ var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
2462
+ function baseName(file) {
2463
+ let name = file;
2464
+ for (const ext of ROUTE_EXTENSIONS) {
2465
+ if (name.endsWith(ext)) {
2466
+ name = name.slice(0, -ext.length);
2467
+ break;
2468
+ }
2469
+ }
2470
+ for (const suffix of PLATFORM_SUFFIXES) {
2471
+ if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
2472
+ }
2473
+ return name;
2474
+ }
2475
+ function isRouteFile(file) {
2476
+ return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
2477
+ }
2478
+ function isSpecial(base) {
2479
+ if (base === "_layout") return true;
2480
+ if (base.startsWith("+")) return base !== "+not-found";
2481
+ return base.startsWith("_");
2482
+ }
2483
+ function layoutType(file) {
2484
+ try {
2485
+ const src = fs$1.readFileSync(file, "utf-8");
2486
+ if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
2487
+ if (/<Drawer\b/.test(src)) return "drawer";
2488
+ } catch {
2489
+ }
2490
+ return "stack";
2491
+ }
2492
+ function findLayout(dir) {
2493
+ for (const ext of ROUTE_EXTENSIONS) {
2494
+ const candidate = path2__namespace.default.join(dir, "_layout" + ext);
2495
+ if (fs$1.existsSync(candidate)) return candidate;
2496
+ }
2497
+ return null;
2498
+ }
2499
+ function analyzeExpoRouter(appRoot) {
2500
+ let routeDir = null;
2501
+ for (const candidate of ROUTE_DIR_CANDIDATES) {
2502
+ const full = path2__namespace.default.join(appRoot, candidate);
2503
+ if (fs$1.existsSync(full) && findLayout(full)) {
2504
+ routeDir = full;
2505
+ break;
2506
+ }
2507
+ }
2508
+ if (!routeDir) return null;
2509
+ const routes = [];
2510
+ const navigators = /* @__PURE__ */ new Map();
2511
+ const seen = /* @__PURE__ */ new Set();
2512
+ const walk = (dir, segments, navigator) => {
2513
+ const layout = findLayout(dir);
2514
+ const current = layout ? segments.join("/") || "/" : navigator;
2515
+ if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
2516
+ let entries;
2517
+ try {
2518
+ entries = fs$1.readdirSync(dir);
2519
+ } catch {
2520
+ return;
2521
+ }
2522
+ for (const entry of entries.sort()) {
2523
+ const full = path2__namespace.default.join(dir, entry);
2524
+ let isDir = false;
2525
+ try {
2526
+ isDir = fs$1.statSync(full).isDirectory();
2527
+ } catch {
2528
+ continue;
2529
+ }
2530
+ if (isDir) {
2531
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
2532
+ walk(full, [...segments, entry], current);
2533
+ continue;
2534
+ }
2535
+ if (!isRouteFile(entry)) continue;
2536
+ const base = baseName(entry);
2537
+ if (isSpecial(base)) continue;
2538
+ const routeSegments = base === "index" ? segments : [...segments, base];
2539
+ const name = "/" + routeSegments.join("/");
2540
+ if (seen.has(name)) continue;
2541
+ seen.add(name);
2542
+ routes.push({
2543
+ name,
2544
+ componentFile: full,
2545
+ navigatorName: current,
2546
+ navigatorType: navigators.get(current) ?? "stack",
2547
+ params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
2548
+ });
2549
+ }
2550
+ };
2551
+ walk(routeDir, [], "/");
2552
+ return {
2553
+ routeDir: path2__namespace.default.relative(appRoot, routeDir),
2554
+ routes,
2555
+ navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
2556
+ };
2557
+ }
2558
+
2559
+ // src/analyzers/NavigationAnalyzer.ts
2560
+ var NAVIGATOR_FACTORIES = {
2561
+ createStackNavigator: "stack",
2562
+ createNativeStackNavigator: "stack",
2563
+ createBottomTabNavigator: "tab",
2564
+ createTabNavigator: "tab",
2565
+ createMaterialTopTabNavigator: "tab",
2566
+ createMaterialBottomTabNavigator: "tab",
2567
+ createDrawerNavigator: "drawer"
2568
+ };
2569
+ var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
2570
+ function unwrapStaticScreen(value) {
2571
+ if (BabelTypes__namespace.isCallExpression(value)) {
2572
+ const arg = value.arguments[0];
2573
+ return arg ? unwrapStaticScreen(arg) : null;
2574
+ }
2575
+ if (BabelTypes__namespace.isObjectExpression(value)) {
2576
+ for (const prop of value.properties) {
2577
+ if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
2578
+ const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : null;
2579
+ if (key === "screen" && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
2580
+ }
2581
+ return null;
2582
+ }
2583
+ return BabelTypes__namespace.isExpression(value) ? value : null;
2584
+ }
2585
+ function inferNavigatorType(factoryName) {
2586
+ if (/drawer/i.test(factoryName)) return "drawer";
2587
+ if (/tab/i.test(factoryName)) return "tab";
2588
+ return "stack";
2589
+ }
2590
+ var NAVIGATION_EVIDENCE = new RegExp(
2591
+ // `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
2592
+ // não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
2593
+ // `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
2594
+ // que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
2595
+ // junção entre arquivos nunca chega a ser tentada.
2596
+ // Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
2597
+ // fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
2598
+ // seis navegadores do `bluesky`.
2599
+ `create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
2600
+ );
1663
2601
  var NavigationAnalyzer = class {
1664
2602
  config;
1665
2603
  /** Extra glob patterns for navigation file discovery (added to defaults) */
1666
2604
  navigationInclude;
1667
2605
  /** Extra glob patterns to exclude from navigation analysis */
1668
2606
  navigationExclude;
2607
+ /** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
2608
+ graph;
2609
+ /**
2610
+ * Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
2611
+ * de navegação e a árvore de telas; sem ela as duas metades do documento
2612
+ * falam de coisas diferentes. Populada por `analyze()`.
2613
+ */
2614
+ routeTargets = /* @__PURE__ */ new Map();
2615
+ /**
2616
+ * Arquivos que DECLARAM uma fábrica de navegador.
2617
+ *
2618
+ * Um navegador aninhado é montado como `component=` de uma rota — no
2619
+ * `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
2620
+ * Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
2621
+ * exatamente o falso positivo que o corpus existe para não repetir. Um
2622
+ * navegador é um contêiner de rotas; a tela está um nível abaixo.
2623
+ *
2624
+ * Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
2625
+ * RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
2626
+ * `DetailViewStack` num arquivo e escreve o JSX em
2627
+ * `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
2628
+ * segundo entrar como tela.
2629
+ */
2630
+ navigatorFiles = /* @__PURE__ */ new Set();
2631
+ /** Preenchido por `analyze()`. Vazio antes disso. */
2632
+ diagnostics = {
2633
+ filesScanned: 0,
2634
+ filesWithEvidence: 0,
2635
+ navigatorsDeclared: 0,
2636
+ navigatorsWithJsx: 0,
2637
+ routes: 0,
2638
+ routesLinkedToFile: 0
2639
+ };
1669
2640
  constructor(config, options) {
1670
2641
  this.config = config;
2642
+ this.graph = new ModuleGraph(config.rootDir);
1671
2643
  this.navigationInclude = options?.navigationInclude ?? [];
1672
2644
  this.navigationExclude = options?.navigationExclude ?? [];
1673
2645
  }
1674
2646
  /** Build the full navigation graph */
1675
2647
  async analyze() {
1676
2648
  const navigationFiles = await this.findNavigationFiles();
1677
- const parsedNavigators = [];
1678
2649
  const typeExports = [];
2650
+ this.routeTargets.clear();
2651
+ const declarations = /* @__PURE__ */ new Map();
2652
+ const evidenceFiles = [];
2653
+ this.navigatorFiles.clear();
1679
2654
  for (const filePath of navigationFiles) {
1680
2655
  try {
1681
2656
  const content = await fs$1.promises.readFile(filePath, "utf-8");
1682
- const navigators = this.parseNavigators(content, filePath);
1683
- const types = this.parseParamTypes(content, filePath);
1684
- parsedNavigators.push(...navigators);
1685
- typeExports.push(...types);
2657
+ if (!NAVIGATION_EVIDENCE.test(content)) continue;
2658
+ evidenceFiles.push(filePath);
2659
+ for (const decl of this.collectDeclarations(filePath)) {
2660
+ declarations.set(`${decl.file}#${decl.name}`, decl);
2661
+ if (decl.confident) this.navigatorFiles.add(decl.file);
2662
+ }
2663
+ typeExports.push(...this.parseParamTypes(content, filePath));
1686
2664
  } catch (error) {
1687
2665
  console.warn(`Failed to parse ${filePath}:`, error);
1688
2666
  }
1689
2667
  }
2668
+ const parsedNavigators = [];
2669
+ for (const filePath of evidenceFiles) {
2670
+ try {
2671
+ parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
2672
+ } catch (error) {
2673
+ console.warn(`Failed to parse navigators in ${filePath}:`, error);
2674
+ }
2675
+ }
2676
+ let detachedScreens = 0;
2677
+ for (const filePath of evidenceFiles) {
2678
+ try {
2679
+ for (const detached of this.parseDetachedScreens(filePath, declarations)) {
2680
+ const existing = parsedNavigators.find(
2681
+ (nav) => nav.name === detached.name && nav.type === detached.type
2682
+ );
2683
+ if (!existing) {
2684
+ parsedNavigators.push(detached);
2685
+ detachedScreens += detached.screens.length;
2686
+ continue;
2687
+ }
2688
+ const seen = new Set(existing.screens.map((screen) => screen.name));
2689
+ for (const screen of detached.screens) {
2690
+ if (seen.has(screen.name)) continue;
2691
+ seen.add(screen.name);
2692
+ existing.screens.push(screen);
2693
+ detachedScreens++;
2694
+ }
2695
+ }
2696
+ } catch (error) {
2697
+ console.warn(`Failed to parse detached screens in ${filePath}:`, error);
2698
+ }
2699
+ }
2700
+ const expo = analyzeExpoRouter(this.config.rootDir);
2701
+ if (expo) {
2702
+ console.log(
2703
+ `[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
2704
+ );
2705
+ this.diagnostics.fileBasedRouter = {
2706
+ kind: "expo-router",
2707
+ routeDir: expo.routeDir,
2708
+ routes: expo.routes.length
2709
+ };
2710
+ const byNavigator = /* @__PURE__ */ new Map();
2711
+ for (const nav of expo.navigators) {
2712
+ byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
2713
+ }
2714
+ for (const route of expo.routes) {
2715
+ const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
2716
+ name: route.navigatorName,
2717
+ type: route.navigatorType,
2718
+ screens: []
2719
+ }).get(route.navigatorName);
2720
+ navigator.screens.push({
2721
+ name: route.name,
2722
+ navigatorName: route.navigatorName,
2723
+ navigatorType: route.navigatorType,
2724
+ componentFile: route.componentFile,
2725
+ ...route.params.length ? {
2726
+ params: route.params.map((name) => ({ name, type: "string", required: true }))
2727
+ } : {}
2728
+ });
2729
+ this.routeTargets.set(route.name, route.componentFile);
2730
+ }
2731
+ parsedNavigators.push(...byNavigator.values());
2732
+ }
2733
+ let staticOnly = 0;
2734
+ for (const decl of declarations.values()) {
2735
+ if (!decl.confident || !decl.staticScreens?.length) continue;
2736
+ const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
2737
+ if (existing) {
2738
+ const seen = new Set(existing.screens.map((s) => s.name));
2739
+ for (const screen of decl.staticScreens) {
2740
+ if (!seen.has(screen.name)) existing.screens.push(screen);
2741
+ }
2742
+ continue;
2743
+ }
2744
+ parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
2745
+ staticOnly++;
2746
+ }
2747
+ const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
2748
+ const uniqueRoutes = new Set(
2749
+ parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
2750
+ );
2751
+ this.diagnostics = {
2752
+ ...this.diagnostics,
2753
+ filesScanned: navigationFiles.length,
2754
+ filesWithEvidence: evidenceFiles.length,
2755
+ navigatorsDeclared: confidentDeclarations,
2756
+ navigatorsWithJsx: parsedNavigators.length,
2757
+ routes: uniqueRoutes.size,
2758
+ // `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
2759
+ // registrada por um navegador que não sobreviveu. O mínimo evita a
2760
+ // cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
2761
+ routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
2762
+ };
2763
+ console.log(
2764
+ `[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`
2765
+ );
1690
2766
  this.attachParamsToNavigators(parsedNavigators, typeExports);
1691
2767
  return this.buildNavigationGraph(parsedNavigators);
1692
2768
  }
1693
- /** Find all navigation-related files */
2769
+ /** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
2770
+ collectDeclarations(filePath) {
2771
+ const ast = this.graph.parse(filePath);
2772
+ if (!ast) return [];
2773
+ const out = /* @__PURE__ */ new Map();
2774
+ traverse5__default.default(ast, {
2775
+ CallExpression: (nodePath) => {
2776
+ const callee = nodePath.node.callee;
2777
+ if (!BabelTypes__namespace.isIdentifier(callee)) return;
2778
+ const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
2779
+ const type = known ?? inferNavigatorType(callee.name);
2780
+ const confident = known !== null;
2781
+ let up = nodePath.parentPath;
2782
+ for (let i = 0; i < 4 && up; i++) {
2783
+ if (BabelTypes__namespace.isVariableDeclarator(up.node)) break;
2784
+ if (!BabelTypes__namespace.isMemberExpression(up.node) && !BabelTypes__namespace.isCallExpression(up.node)) {
2785
+ up = null;
2786
+ break;
2787
+ }
2788
+ up = up.parentPath;
2789
+ }
2790
+ const declarator = up?.node;
2791
+ if (!declarator || !BabelTypes__namespace.isVariableDeclarator(declarator) || !BabelTypes__namespace.isIdentifier(declarator.id)) {
2792
+ return;
2793
+ }
2794
+ const name = declarator.id.name;
2795
+ if (out.get(name)?.confident) return;
2796
+ const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
2797
+ out.set(name, {
2798
+ file: filePath,
2799
+ name,
2800
+ type,
2801
+ confident,
2802
+ ...staticScreens.length ? { staticScreens } : {}
2803
+ });
2804
+ }
2805
+ });
2806
+ return [...out.values()];
2807
+ }
2808
+ /**
2809
+ * As rotas de `createXNavigator({ screens: { … } })`.
2810
+ *
2811
+ * Quatro formas no corpus, todas em `rocketchat`:
2812
+ * `NewServerView` — shorthand, nome = componente
2813
+ * `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
2814
+ * `SelectListView: SelectListViewScreen` — identificador direto
2815
+ * `ChatsStackNavigator: ChatsStack` — navegador aninhado
2816
+ *
2817
+ * `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
2818
+ * e agrupar rotas não muda o que elas são.
2819
+ */
2820
+ parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
2821
+ if (!config || !BabelTypes__namespace.isObjectExpression(config) || depth > 4) return [];
2822
+ const out = [];
2823
+ for (const prop of config.properties) {
2824
+ if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
2825
+ const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
2826
+ if (!key) continue;
2827
+ if (key === "groups" && BabelTypes__namespace.isObjectExpression(prop.value)) {
2828
+ for (const group of prop.value.properties) {
2829
+ if (!BabelTypes__namespace.isObjectProperty(group) || !BabelTypes__namespace.isExpression(group.value)) continue;
2830
+ out.push(
2831
+ ...this.parseStaticScreens(
2832
+ filePath,
2833
+ group.value,
2834
+ navigatorName,
2835
+ navigatorType,
2836
+ depth + 1
2837
+ )
2838
+ );
2839
+ }
2840
+ continue;
2841
+ }
2842
+ if (key !== "screens" || !BabelTypes__namespace.isObjectExpression(prop.value)) continue;
2843
+ for (const entry of prop.value.properties) {
2844
+ if (!BabelTypes__namespace.isObjectProperty(entry)) continue;
2845
+ const routeName = BabelTypes__namespace.isIdentifier(entry.key) ? entry.key.name : BabelTypes__namespace.isStringLiteral(entry.key) ? entry.key.value : null;
2846
+ if (!routeName) continue;
2847
+ const componentExpr = unwrapStaticScreen(entry.value);
2848
+ const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
2849
+ if (componentFile) this.routeTargets.set(routeName, componentFile);
2850
+ out.push({
2851
+ name: routeName,
2852
+ navigatorName,
2853
+ navigatorType,
2854
+ ...componentFile ? { componentFile } : {}
2855
+ });
2856
+ }
2857
+ }
2858
+ return out;
2859
+ }
2860
+ /**
2861
+ * Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
2862
+ * certo.
2863
+ *
2864
+ * POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
2865
+ * `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
2866
+ * convenção de caminho. Medido contra 20 apps React Native de terceiros
2867
+ * (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
2868
+ * app não ter navegação:
2869
+ *
2870
+ * - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
2871
+ * - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
2872
+ * sensível a caixa;
2873
+ * - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
2874
+ * pedia um arquivo chamado `routes*`;
2875
+ * - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
2876
+ * - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
2877
+ *
2878
+ * Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
2879
+ * globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
2880
+ * O que segura o custo aqui é o portão por evidência em `analyze()`, que só
2881
+ * parseia arquivo cujo texto menciona uma fábrica de navegador.
2882
+ *
2883
+ * Os globs antigos continuam na lista: se um projeto restringir `include`, o
2884
+ * que era encontrado antes continua sendo.
2885
+ */
1694
2886
  async findNavigationFiles() {
1695
2887
  const patterns = [
2888
+ ...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
2889
+ // Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
1696
2890
  "**/navigation/**/*.{ts,tsx}",
1697
2891
  "**/navigator*.{ts,tsx}",
1698
2892
  "**/routes*.{ts,tsx}",
@@ -1703,91 +2897,188 @@ var NavigationAnalyzer = class {
1703
2897
  "**/node_modules/**",
1704
2898
  "**/dist/**",
1705
2899
  "**/build/**",
2900
+ // Um navegador declarado dentro de um teste é fixture, não a navegação
2901
+ // do app. Isto passou a importar quando a varredura deixou de ser por
2902
+ // caminho: em `expensify`, os únicos arquivos que o glob antigo
2903
+ // alcançava eram três de `tests/`.
2904
+ "**/__tests__/**",
2905
+ "**/test/**",
2906
+ "**/tests/**",
2907
+ "**/*.test.{ts,tsx,js,jsx}",
2908
+ "**/*.tests.{ts,tsx,js,jsx}",
2909
+ "**/*.spec.{ts,tsx,js,jsx}",
2910
+ // Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
2911
+ // `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
2912
+ // Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
2913
+ // destinos que existem no teste e não no app, para os quais o agente
2914
+ // tentaria navegar.
2915
+ "**/*Test.{ts,tsx,js,jsx}",
2916
+ "**/*Tests.{ts,tsx,js,jsx}",
1706
2917
  ...this.config.exclude || [],
1707
2918
  // §5: Add user-configured navigation excludes
1708
2919
  ...this.navigationExclude
1709
2920
  ];
1710
- const files = await glob__default.default(patterns, {
2921
+ const files = await globSorted(patterns, {
1711
2922
  cwd: this.config.rootDir,
1712
2923
  ignore: excludePatterns
1713
2924
  });
1714
- return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
2925
+ return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
1715
2926
  }
1716
- /** Parse navigator definitions from a file */
1717
- parseNavigators(content, filePath) {
1718
- const navigators = [];
1719
- try {
1720
- const ast = parser__namespace.parse(content, {
1721
- sourceType: "module",
1722
- plugins: [
1723
- "jsx",
1724
- "typescript",
1725
- ...this.config.parserPlugins || []
1726
- ]
1727
- });
1728
- const navigatorCalls = /* @__PURE__ */ new Map();
1729
- const screensByNavigator = /* @__PURE__ */ new Map();
1730
- traverse4__default.default(ast, {
1731
- // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
1732
- CallExpression: (nodePath) => {
1733
- const { node } = nodePath;
1734
- const callee = node.callee;
1735
- let navigatorType = null;
1736
- if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
1737
- navigatorType = "stack";
1738
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createStackNavigator") {
1739
- navigatorType = "stack";
1740
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
1741
- navigatorType = "tab";
1742
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createTabNavigator") {
1743
- navigatorType = "tab";
1744
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
1745
- navigatorType = "drawer";
1746
- }
1747
- if (navigatorType) {
1748
- const parent = nodePath.parent;
1749
- if (BabelTypes__namespace.isVariableDeclarator(parent) && BabelTypes__namespace.isIdentifier(parent.id)) {
1750
- const varName = parent.id.name;
1751
- navigatorCalls.set(varName, {
1752
- name: varName,
1753
- type: navigatorType,
1754
- screens: []
1755
- });
1756
- }
1757
- }
1758
- },
1759
- // Detect Stack.Navigator / Tab.Navigator JSX elements
1760
- JSXElement: (nodePath) => {
1761
- const { node } = nodePath;
1762
- const openingElement = node.openingElement;
1763
- if (BabelTypes__namespace.isJSXMemberExpression(openingElement.name) && BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
1764
- const objectName = openingElement.name.object.name;
1765
- const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
1766
- if (propertyName === "Navigator") {
1767
- const navigator = navigatorCalls.get(objectName);
1768
- if (navigator) {
1769
- const initialRouteAttr = openingElement.attributes.find(
1770
- (attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
1771
- );
1772
- if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr) && BabelTypes__namespace.isStringLiteral(initialRouteAttr.value)) {
1773
- navigator.initialRouteName = initialRouteAttr.value.value;
1774
- }
1775
- const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
1776
- screensByNavigator.set(objectName, screens);
1777
- }
1778
- }
1779
- }
2927
+ /**
2928
+ * Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
2929
+ * que pode estar aqui ou em qualquer arquivo que este importe.
2930
+ */
2931
+ parseNavigatorUsages(filePath, declarations) {
2932
+ const ast = this.graph.parse(filePath);
2933
+ if (!ast) return [];
2934
+ const found = /* @__PURE__ */ new Map();
2935
+ traverse5__default.default(ast, {
2936
+ JSXElement: (nodePath) => {
2937
+ const { node } = nodePath;
2938
+ const openingElement = node.openingElement;
2939
+ if (!BabelTypes__namespace.isJSXMemberExpression(openingElement.name) || !BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
2940
+ return;
1780
2941
  }
1781
- });
1782
- navigatorCalls.forEach((navigator) => {
1783
- const screens = screensByNavigator.get(navigator.name) || [];
1784
- navigator.screens = screens;
1785
- navigators.push(navigator);
1786
- });
1787
- } catch (error) {
1788
- console.warn(`Failed to parse navigators in ${filePath}:`, error);
2942
+ const objectName = openingElement.name.object.name;
2943
+ const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2944
+ if (propertyName !== "Navigator") return;
2945
+ const decl = this.resolveNavigator(filePath, objectName, declarations);
2946
+ if (!decl) return;
2947
+ const navigator = found.get(objectName) ?? {
2948
+ name: objectName,
2949
+ type: decl.type,
2950
+ screens: []
2951
+ };
2952
+ const initialRouteAttr = openingElement.attributes.find(
2953
+ (attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2954
+ );
2955
+ if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr)) {
2956
+ const initial = this.attributeString(filePath, initialRouteAttr);
2957
+ if (initial) navigator.initialRouteName = initial;
2958
+ }
2959
+ this.navigatorFiles.add(filePath);
2960
+ this.navigatorFiles.add(decl.file);
2961
+ navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
2962
+ found.set(objectName, navigator);
2963
+ }
2964
+ });
2965
+ return [...found.values()];
2966
+ }
2967
+ /**
2968
+ * Rotas declaradas FORA de qualquer `<X.Navigator>`.
2969
+ *
2970
+ * O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
2971
+ * que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
2972
+ * dois são 171 rotas invisíveis:
2973
+ *
2974
+ * `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
2975
+ * <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
2976
+ * dentro de seis navegadores diferentes. 70 rotas.
2977
+ * `comapeo` — `export const createAppScreens = ({intl}) => (<>
2978
+ * <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
2979
+ * num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
2980
+ *
2981
+ * A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
2982
+ * navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
2983
+ * literal do fonte (ou constante que o resolvedor segue), e `X` precisa
2984
+ * chegar a uma declaração de navegador que já existe. O que não resolve não
2985
+ * vira nada.
2986
+ *
2987
+ * O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
2988
+ * fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
2989
+ *
2990
+ * DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
2991
+ * primeira é a de sempre (declaração local, ou `import` seguido até a
2992
+ * origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
2993
+ * parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
2994
+ * o navegador `Flat` é o próprio app, no fonte.
2995
+ *
2996
+ * O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
2997
+ * contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
2998
+ * cinco stacks que também chamam a mesma função. Seguir os seis pontos de
2999
+ * chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
3000
+ * da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
3001
+ */
3002
+ parseDetachedScreens(filePath, declarations) {
3003
+ const ast = this.graph.parse(filePath);
3004
+ if (!ast) return [];
3005
+ const byNavigator = /* @__PURE__ */ new Map();
3006
+ const resolved = /* @__PURE__ */ new Map();
3007
+ traverse5__default.default(ast, {
3008
+ JSXElement: (nodePath) => {
3009
+ const name = nodePath.node.openingElement.name;
3010
+ if (!BabelTypes__namespace.isJSXMemberExpression(name) || !BabelTypes__namespace.isJSXIdentifier(name.object)) return;
3011
+ if (!BabelTypes__namespace.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
3012
+ const insideNavigator = nodePath.findParent((parent) => {
3013
+ if (!parent.isJSXElement()) return false;
3014
+ const parentName = parent.node.openingElement.name;
3015
+ return BabelTypes__namespace.isJSXMemberExpression(parentName) && BabelTypes__namespace.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
3016
+ });
3017
+ if (insideNavigator) return;
3018
+ const local = name.object.name;
3019
+ if (!resolved.has(local)) {
3020
+ resolved.set(
3021
+ local,
3022
+ this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
3023
+ );
3024
+ }
3025
+ const decl = resolved.get(local);
3026
+ if (!decl) return;
3027
+ const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
3028
+ if (!screen) return;
3029
+ const nav = byNavigator.get(decl.name) ?? {
3030
+ name: decl.name,
3031
+ type: decl.type,
3032
+ screens: []
3033
+ };
3034
+ if (!nav.screens.some((existing) => existing.name === screen.name))
3035
+ nav.screens.push(screen);
3036
+ byNavigator.set(decl.name, nav);
3037
+ }
3038
+ });
3039
+ return [...byNavigator.values()];
3040
+ }
3041
+ /**
3042
+ * De um `X` usado como `<X.Screen>` até a declaração do navegador.
3043
+ *
3044
+ * Além do caminho normal — declaração local ou `import` seguido até a origem
3045
+ * — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
3046
+ * `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
3047
+ */
3048
+ resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
3049
+ const direct = this.resolveNavigator(filePath, localName, declarations);
3050
+ if (direct) return direct;
3051
+ const binding = nodePath.scope.getBinding(localName);
3052
+ if (!binding || binding.kind !== "param") return null;
3053
+ const param = binding.path.node;
3054
+ if (!BabelTypes__namespace.isIdentifier(param) || !BabelTypes__namespace.isTSTypeAnnotation(param.typeAnnotation)) return null;
3055
+ const annotation = param.typeAnnotation.typeAnnotation;
3056
+ if (!BabelTypes__namespace.isTSTypeQuery(annotation) || !BabelTypes__namespace.isIdentifier(annotation.exprName)) return null;
3057
+ return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
3058
+ }
3059
+ /**
3060
+ * De `<X.Navigator>` até a declaração de `X`.
3061
+ *
3062
+ * Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
3063
+ * é definido. Resolver pelo import é o que torna a junção entre arquivos
3064
+ * segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
3065
+ * é o arquivo de DEFINIÇÃO.
3066
+ */
3067
+ resolveNavigator(filePath, localName, declarations) {
3068
+ const local = declarations.get(`${filePath}#${localName}`);
3069
+ if (local) return local;
3070
+ const origin = this.graph.resolveBinding(filePath, localName);
3071
+ if (!origin) return null;
3072
+ return declarations.get(`${origin.file}#${origin.name}`) ?? null;
3073
+ }
3074
+ /** O valor string de um atributo JSX, resolvendo constante importada. */
3075
+ attributeString(filePath, attr) {
3076
+ const value = attr.value;
3077
+ if (BabelTypes__namespace.isStringLiteral(value)) return value.value;
3078
+ if (BabelTypes__namespace.isJSXExpressionContainer(value) && BabelTypes__namespace.isExpression(value.expression)) {
3079
+ return this.graph.stringConstant(filePath, value.expression);
1789
3080
  }
1790
- return navigators;
3081
+ return null;
1791
3082
  }
1792
3083
  /**
1793
3084
  * Is this JSX element `<navigatorVarName.MEMBER …>`?
@@ -1882,22 +3173,48 @@ var NavigationAnalyzer = class {
1882
3173
  return found;
1883
3174
  }
1884
3175
  /** Extract screens from a navigator JSX element */
1885
- extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
3176
+ extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
1886
3177
  const screens = [];
1887
3178
  if (!navigatorElement.children) return screens;
1888
3179
  const seen = /* @__PURE__ */ new Set();
1889
3180
  for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
1890
- const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
1891
- if (!screenName || seen.has(screenName)) continue;
1892
- seen.add(screenName);
1893
- screens.push({
1894
- name: screenName,
1895
- navigatorName: navigatorVarName,
1896
- navigatorType
1897
- });
3181
+ const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
3182
+ if (!screen || seen.has(screen.name)) continue;
3183
+ seen.add(screen.name);
3184
+ screens.push(screen);
1898
3185
  }
1899
3186
  return screens;
1900
3187
  }
3188
+ /**
3189
+ * Uma `<X.Screen>` isolada até a rota que ela declara.
3190
+ *
3191
+ * Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
3192
+ * a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
3193
+ * dele — só a forma de chegar até o elemento muda.
3194
+ */
3195
+ parseScreenElement(filePath, element, navigatorName, navigatorType) {
3196
+ const attrs = element.openingElement.attributes;
3197
+ const nameAttr = attrs.find(
3198
+ (a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "name"
3199
+ );
3200
+ const screenName = BabelTypes__namespace.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
3201
+ if (!screenName) return null;
3202
+ const componentAttr = attrs.find(
3203
+ (a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "component"
3204
+ );
3205
+ let componentFile = null;
3206
+ if (BabelTypes__namespace.isJSXAttribute(componentAttr) && BabelTypes__namespace.isJSXExpressionContainer(componentAttr.value)) {
3207
+ const expr = componentAttr.value.expression;
3208
+ if (BabelTypes__namespace.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
3209
+ }
3210
+ if (componentFile) this.routeTargets.set(screenName, componentFile);
3211
+ return {
3212
+ name: screenName,
3213
+ navigatorName,
3214
+ navigatorType,
3215
+ ...componentFile ? { componentFile } : {}
3216
+ };
3217
+ }
1901
3218
  /** Extract string attribute value from JSX attributes */
1902
3219
  extractAttributeValue(attributes, attrName) {
1903
3220
  const attr = attributes.find(
@@ -1920,7 +3237,7 @@ var NavigationAnalyzer = class {
1920
3237
  ...this.config.parserPlugins || []
1921
3238
  ]
1922
3239
  });
1923
- traverse4__default.default(ast, {
3240
+ traverse5__default.default(ast, {
1924
3241
  TSTypeAliasDeclaration: (nodePath) => {
1925
3242
  const { node } = nodePath;
1926
3243
  const typeName = node.id.name;
@@ -1993,7 +3310,7 @@ var NavigationAnalyzer = class {
1993
3310
  if (type.type === "TSUndefinedKeyword") return "undefined";
1994
3311
  if (type.type === "TSNullKeyword") return "null";
1995
3312
  if (type.type === "TSUnionType") {
1996
- return type.types.map((t12) => this.typeToString(t12)).join(" | ");
3313
+ return type.types.map((t15) => this.typeToString(t15)).join(" | ");
1997
3314
  }
1998
3315
  if (type.type === "TSTypeLiteral") {
1999
3316
  return "object";
@@ -2014,7 +3331,7 @@ var NavigationAnalyzer = class {
2014
3331
  /** Attach parsed type params to navigator screens */
2015
3332
  attachParamsToNavigators(navigators, types) {
2016
3333
  for (const navigator of navigators) {
2017
- const matchingType = types.find((t12) => t12.type === navigator.type);
3334
+ const matchingType = types.find((t15) => t15.type === navigator.type);
2018
3335
  if (matchingType) {
2019
3336
  for (const screen of navigator.screens) {
2020
3337
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -2103,9 +3420,9 @@ var ComponentAnalyzer = class {
2103
3420
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
2104
3421
  });
2105
3422
  const components = [];
2106
- traverse4__default.default(ast, {
2107
- JSXElement: (path9) => {
2108
- const component = this.extractComponentFromJSXElement(path9.node);
3423
+ traverse5__default.default(ast, {
3424
+ JSXElement: (path11) => {
3425
+ const component = this.extractComponentFromJSXElement(path11.node);
2109
3426
  if (component) {
2110
3427
  components.push(component);
2111
3428
  }
@@ -2231,14 +3548,14 @@ var FormAnalyzer = class {
2231
3548
  this.stateVariables.clear();
2232
3549
  this.inputElements = [];
2233
3550
  this.submitButtons = [];
2234
- traverse4__default.default(ast, {
2235
- CallExpression: (path9) => {
2236
- this.extractStateVariables(path9.node);
3551
+ traverse5__default.default(ast, {
3552
+ CallExpression: (path11) => {
3553
+ this.extractStateVariables(path11.node);
2237
3554
  }
2238
3555
  });
2239
- traverse4__default.default(ast, {
2240
- JSXElement: (path9) => {
2241
- this.extractFormElements(path9.node);
3556
+ traverse5__default.default(ast, {
3557
+ JSXElement: (path11) => {
3558
+ this.extractFormElements(path11.node);
2242
3559
  }
2243
3560
  });
2244
3561
  const validationRules = this.extractValidationRules(ast);
@@ -2272,23 +3589,23 @@ var FormAnalyzer = class {
2272
3589
  for (const attr of openingElement.attributes) {
2273
3590
  if (BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name)) {
2274
3591
  const propName = attr.name.name;
2275
- const propValue = this.extractAttributeValue(attr.value);
3592
+ const propValue2 = this.extractAttributeValue(attr.value);
2276
3593
  switch (propName) {
2277
3594
  case "label":
2278
- info.label = propValue;
3595
+ info.label = propValue2;
2279
3596
  break;
2280
3597
  case "placeholder":
2281
- info.placeholder = propValue;
3598
+ info.placeholder = propValue2;
2282
3599
  break;
2283
3600
  case "keyboardType":
2284
- info.keyboardType = propValue;
3601
+ info.keyboardType = propValue2;
2285
3602
  break;
2286
3603
  case "testID":
2287
- info.testID = propValue;
3604
+ info.testID = propValue2;
2288
3605
  break;
2289
3606
  case "appilotsId":
2290
- info.appilotsId = propValue;
2291
- if (propValue) info.varName = propValue;
3607
+ info.appilotsId = propValue2;
3608
+ if (propValue2) info.varName = propValue2;
2292
3609
  break;
2293
3610
  case "value":
2294
3611
  if (attr.value && BabelTypes__namespace.isJSXExpressionContainer(attr.value) && BabelTypes__namespace.isIdentifier(attr.value.expression)) {
@@ -2337,9 +3654,9 @@ var FormAnalyzer = class {
2337
3654
  }
2338
3655
  extractValidationRules(ast) {
2339
3656
  const rules = {};
2340
- traverse4__default.default(ast, {
2341
- IfStatement: (path9) => {
2342
- const test = path9.node.test;
3657
+ traverse5__default.default(ast, {
3658
+ IfStatement: (path11) => {
3659
+ const test = path11.node.test;
2343
3660
  const rule = this.extractRuleFromCondition(test);
2344
3661
  if (rule) {
2345
3662
  const { field, description } = rule;
@@ -2393,7 +3710,7 @@ var FormAnalyzer = class {
2393
3710
  }
2394
3711
  buildForms(filePath, validationRules) {
2395
3712
  if (this.inputElements.length === 0) return [];
2396
- const fileName = path8__namespace.basename(filePath, path8__namespace.extname(filePath));
3713
+ const fileName = path2__namespace.basename(filePath, path2__namespace.extname(filePath));
2397
3714
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2398
3715
  const fields = this.inputElements.map((input) => {
2399
3716
  const fieldType = this.inferFieldType(input);
@@ -2442,13 +3759,105 @@ var FormAnalyzer = class {
2442
3759
  return rule !== void 0 && rule.includes("required");
2443
3760
  }
2444
3761
  };
3762
+
3763
+ // src/pipeline/composition.ts
3764
+ var MAX_DEPTH = 3;
3765
+ var MAX_VISITED_PER_SCREEN = 60;
3766
+ var MAX_FAN_IN = 3;
3767
+ var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
3768
+ function descendants(graph, from, screenFiles) {
3769
+ const seen = /* @__PURE__ */ new Set([from]);
3770
+ const out = [];
3771
+ let frontier = [from];
3772
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
3773
+ const next = [];
3774
+ for (const file of frontier) {
3775
+ for (const child of graph.renderedComponentFiles(file)) {
3776
+ if (seen.has(child)) continue;
3777
+ seen.add(child);
3778
+ if (screenFiles.has(child)) continue;
3779
+ out.push(child);
3780
+ next.push(child);
3781
+ if (out.length >= MAX_VISITED_PER_SCREEN) return out;
3782
+ }
3783
+ }
3784
+ frontier = next;
3785
+ }
3786
+ return out;
3787
+ }
3788
+ function addressable(locator) {
3789
+ if (!locator || typeof locator !== "object") return false;
3790
+ const l = locator;
3791
+ if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
3792
+ return typeof l["testID"] === "string" && l["testID"].length > 0;
3793
+ }
3794
+ async function composeAffordances(options) {
3795
+ const { screens, graph, analyzeFile, screenFiles } = options;
3796
+ const byScreen = /* @__PURE__ */ new Map();
3797
+ const fanIn = /* @__PURE__ */ new Map();
3798
+ for (const screen of screens) {
3799
+ if (!screen.filePath) continue;
3800
+ const children = descendants(graph, screen.filePath, screenFiles);
3801
+ byScreen.set(screen, children);
3802
+ for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
3803
+ }
3804
+ const analyzed = /* @__PURE__ */ new Map();
3805
+ let screensEnriched = 0;
3806
+ let filesMerged = 0;
3807
+ for (const [screen, children] of byScreen) {
3808
+ const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
3809
+ if (exclusive.length === 0) continue;
3810
+ const actionIds = new Set(screen.actions.map((a) => a.id));
3811
+ const targetIds = new Set((screen.targets ?? []).map((t15) => t15.id));
3812
+ const formIds = new Set(screen.forms.map((f) => f.id));
3813
+ let gained = false;
3814
+ for (const file of exclusive) {
3815
+ if (!analyzed.has(file)) {
3816
+ try {
3817
+ analyzed.set(file, await analyzeFile(file));
3818
+ } catch {
3819
+ analyzed.set(file, null);
3820
+ }
3821
+ }
3822
+ const child = analyzed.get(file);
3823
+ if (!child) continue;
3824
+ let merged = false;
3825
+ for (const action of child.actions) {
3826
+ if (!addressable(action.locator)) continue;
3827
+ if (actionIds.has(action.id)) continue;
3828
+ actionIds.add(action.id);
3829
+ screen.actions.push(action);
3830
+ merged = true;
3831
+ }
3832
+ for (const target of child.targets ?? []) {
3833
+ if (!addressable(target.locator)) continue;
3834
+ if (targetIds.has(target.id)) continue;
3835
+ targetIds.add(target.id);
3836
+ (screen.targets ??= []).push(target);
3837
+ merged = true;
3838
+ }
3839
+ for (const form of child.forms) {
3840
+ if (form.fields.length === 0) continue;
3841
+ const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
3842
+ if (formIds.has(id)) continue;
3843
+ formIds.add(id);
3844
+ screen.forms.push({ ...form, id });
3845
+ merged = true;
3846
+ }
3847
+ if (merged) {
3848
+ filesMerged++;
3849
+ gained = true;
3850
+ }
3851
+ }
3852
+ if (gained) screensEnriched++;
3853
+ }
3854
+ return { screens, screensEnriched, filesMerged };
3855
+ }
3856
+
3857
+ // src/analyzers/ReactNativePlatformAnalyzer.ts
2445
3858
  var ReactNativePlatformAnalyzer = class {
2446
3859
  platform = "react-native";
2447
3860
  async analyze(config, options) {
2448
- const screenAnalyzer = new ScreenAnalyzer(config, {
2449
- strictScreens: options.strictScreens ?? true,
2450
- screenPatterns: options.screenPatterns
2451
- });
2452
3861
  const navigationAnalyzer = new NavigationAnalyzer(config, {
2453
3862
  navigationInclude: options.navigationInclude,
2454
3863
  navigationExclude: options.navigationExclude
@@ -2456,14 +3865,31 @@ var ReactNativePlatformAnalyzer = class {
2456
3865
  const componentAnalyzer = new ComponentAnalyzer(config);
2457
3866
  const formAnalyzer = new FormAnalyzer(config);
2458
3867
  console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
2459
- const [screens, navigation] = await Promise.all([
2460
- screenAnalyzer.analyze(),
2461
- navigationAnalyzer.analyze()
2462
- ]);
3868
+ const navigation = await navigationAnalyzer.analyze();
3869
+ const screenAnalyzer = new ScreenAnalyzer(config, {
3870
+ strictScreens: options.strictScreens ?? true,
3871
+ screenPatterns: options.screenPatterns,
3872
+ // Menos os arquivos que declaram navegador: ver `navigatorFiles`.
3873
+ routeTargetFiles: new Set(
3874
+ [...navigationAnalyzer.routeTargets.values()].filter(
3875
+ (file) => !navigationAnalyzer.navigatorFiles.has(file)
3876
+ )
3877
+ )
3878
+ });
3879
+ const screens = await screenAnalyzer.analyze();
3880
+ const composed = await composeAffordances({
3881
+ screens,
3882
+ graph: navigationAnalyzer.graph,
3883
+ analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
3884
+ screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
3885
+ });
3886
+ console.log(
3887
+ `[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
3888
+ );
2463
3889
  console.log(
2464
3890
  `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
2465
3891
  );
2466
- const screenFiles = await glob__default.default(config.include || ["**/*.tsx", "**/*.ts"], {
3892
+ const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
2467
3893
  cwd: config.rootDir,
2468
3894
  ignore: config.exclude || ["**/node_modules/**"]
2469
3895
  });
@@ -2471,7 +3897,7 @@ var ReactNativePlatformAnalyzer = class {
2471
3897
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
2472
3898
  );
2473
3899
  const enrichmentPromises = screenFiles.map(async (file) => {
2474
- const filePath = path8__namespace.default.resolve(config.rootDir, file);
3900
+ const filePath = path2__namespace.default.resolve(config.rootDir, file);
2475
3901
  try {
2476
3902
  const [components, forms] = await Promise.all([
2477
3903
  componentAnalyzer.analyzeFile(filePath),
@@ -2524,9 +3950,11 @@ var ReactNativePlatformAnalyzer = class {
2524
3950
  });
2525
3951
  return {
2526
3952
  screens: enrichedScreens,
3953
+ controlEvidenceFiles: screenAnalyzer.controlEvidenceFiles,
2527
3954
  navigation,
2528
3955
  analyzedFiles: screenFiles.length,
2529
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
3956
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
3957
+ diagnostics: { navigation: navigationAnalyzer.diagnostics }
2530
3958
  };
2531
3959
  }
2532
3960
  mergeForm(target, source) {
@@ -2544,9 +3972,12 @@ var ReactNativePlatformAnalyzer = class {
2544
3972
  findEquivalentField(fields, incoming) {
2545
3973
  return fields.find((field) => {
2546
3974
  if (field.name && incoming.name && field.name === incoming.name) return true;
2547
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2548
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2549
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
3975
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
3976
+ return true;
3977
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
3978
+ return true;
3979
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
3980
+ return true;
2550
3981
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2551
3982
  return true;
2552
3983
  }
@@ -2579,7 +4010,9 @@ var ReactNativePlatformAnalyzer = class {
2579
4010
  if (incoming.fields.length === 0) return void 0;
2580
4011
  let best;
2581
4012
  for (const form of forms) {
2582
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
4013
+ const overlap = incoming.fields.filter(
4014
+ (field) => this.findEquivalentField(form.fields, field)
4015
+ ).length;
2583
4016
  if (overlap > 0 && (!best || overlap > best.overlap)) {
2584
4017
  best = { form, overlap };
2585
4018
  }
@@ -2733,13 +4166,13 @@ function staticRoutePath(node) {
2733
4166
  if (!node) return void 0;
2734
4167
  if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
2735
4168
  if (BabelTypes__namespace.isTemplateLiteral(node)) {
2736
- let path9 = "";
4169
+ let path11 = "";
2737
4170
  node.quasis.forEach((quasi, index) => {
2738
- path9 += quasi.value.cooked ?? quasi.value.raw;
4171
+ path11 += quasi.value.cooked ?? quasi.value.raw;
2739
4172
  const expr = node.expressions[index];
2740
- if (expr) path9 += `:${paramNameOf(expr)}`;
4173
+ if (expr) path11 += `:${paramNameOf(expr)}`;
2741
4174
  });
2742
- return path9;
4175
+ return path11;
2743
4176
  }
2744
4177
  return void 0;
2745
4178
  }
@@ -2782,7 +4215,7 @@ function extractWebNavigationCalls(ast) {
2782
4215
  calls.push({ method: "navigate", targetPath: node.right.value });
2783
4216
  }
2784
4217
  };
2785
- traverse4__default.default(ast, {
4218
+ traverse5__default.default(ast, {
2786
4219
  noScope: !BabelTypes__namespace.isFile(ast),
2787
4220
  enter: (nodePath) => inspect(nodePath.node)
2788
4221
  });
@@ -2824,17 +4257,17 @@ var WebScreenAnalyzer = class {
2824
4257
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2825
4258
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2826
4259
  } = this.config;
2827
- const files = (await glob__default.default(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
4260
+ const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2828
4261
  (file) => !file.endsWith(".d.ts")
2829
4262
  );
2830
- const patternMatches = await glob__default.default(this.screenPatterns, {
4263
+ const patternMatches = await globSorted(this.screenPatterns, {
2831
4264
  cwd: this.config.rootDir,
2832
4265
  ignore: exclude
2833
4266
  });
2834
- const patternSet = new Set(patternMatches.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
4267
+ const patternSet = new Set(patternMatches.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
2835
4268
  const candidates = [];
2836
4269
  for (const file of files) {
2837
- const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
4270
+ const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
2838
4271
  try {
2839
4272
  const candidate = await this.analyzeFile(filePath);
2840
4273
  if (!candidate) continue;
@@ -2878,7 +4311,7 @@ var WebScreenAnalyzer = class {
2878
4311
  }
2879
4312
  }
2880
4313
  }
2881
- const name = registerScreenMeta?.name || componentName || path8__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
4314
+ const name = registerScreenMeta?.name || componentName || path2__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
2882
4315
  const descriptor = {
2883
4316
  name,
2884
4317
  filePath,
@@ -2904,7 +4337,7 @@ var WebScreenAnalyzer = class {
2904
4337
  // ── registerScreen ────────────────────────────────────────────────
2905
4338
  detectRegisterScreenCall(ast) {
2906
4339
  let found = false;
2907
- traverse4__default.default(ast, {
4340
+ traverse5__default.default(ast, {
2908
4341
  CallExpression: (nodePath) => {
2909
4342
  if (found) return;
2910
4343
  if (isRegisterScreenCallee(nodePath.node.callee)) {
@@ -2917,7 +4350,7 @@ var WebScreenAnalyzer = class {
2917
4350
  }
2918
4351
  extractRegisterScreenMetadata(ast) {
2919
4352
  let plain = null;
2920
- traverse4__default.default(ast, {
4353
+ traverse5__default.default(ast, {
2921
4354
  CallExpression: (nodePath) => {
2922
4355
  if (!isRegisterScreenCallee(nodePath.node.callee)) return;
2923
4356
  const arg = nodePath.node.arguments[0];
@@ -2965,7 +4398,7 @@ var WebScreenAnalyzer = class {
2965
4398
  extractComponentName(ast) {
2966
4399
  let defaultName = "";
2967
4400
  let firstExported = "";
2968
- traverse4__default.default(ast, {
4401
+ traverse5__default.default(ast, {
2969
4402
  ExportDefaultDeclaration: (nodePath) => {
2970
4403
  const declaration = nodePath.node.declaration;
2971
4404
  if (BabelTypes__namespace.isFunctionDeclaration(declaration) && declaration.id?.name) {
@@ -2994,7 +4427,7 @@ var WebScreenAnalyzer = class {
2994
4427
  extractComponents(ast) {
2995
4428
  const components = [];
2996
4429
  const seen = /* @__PURE__ */ new Set();
2997
- traverse4__default.default(ast, {
4430
+ traverse5__default.default(ast, {
2998
4431
  JSXOpeningElement: (nodePath) => {
2999
4432
  const element = nodePath.node;
3000
4433
  const name = getJsxElementName(element);
@@ -3015,7 +4448,7 @@ var WebScreenAnalyzer = class {
3015
4448
  // ── <label htmlFor> association ───────────────────────────────────
3016
4449
  collectHtmlForLabels(ast) {
3017
4450
  const labels = /* @__PURE__ */ new Map();
3018
- traverse4__default.default(ast, {
4451
+ traverse5__default.default(ast, {
3019
4452
  JSXElement: (nodePath) => {
3020
4453
  const element = nodePath.node;
3021
4454
  if (getJsxElementName(element.openingElement) !== "label") return;
@@ -3058,7 +4491,7 @@ var WebScreenAnalyzer = class {
3058
4491
  formBuckets.set(formElement, bucket);
3059
4492
  return bucket;
3060
4493
  };
3061
- traverse4__default.default(ast, {
4494
+ traverse5__default.default(ast, {
3062
4495
  JSXElement: (nodePath) => {
3063
4496
  const element = nodePath.node;
3064
4497
  const name = getJsxElementName(element.openingElement);
@@ -3075,7 +4508,7 @@ var WebScreenAnalyzer = class {
3075
4508
  if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
3076
4509
  }
3077
4510
  });
3078
- traverse4__default.default(ast, {
4511
+ traverse5__default.default(ast, {
3079
4512
  JSXElement: (nodePath) => {
3080
4513
  const element = nodePath.node;
3081
4514
  const name = getJsxElementName(element.openingElement);
@@ -3253,7 +4686,7 @@ var WebScreenAnalyzer = class {
3253
4686
  const actionLabels = new Map(
3254
4687
  actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
3255
4688
  );
3256
- traverse4__default.default(ast, {
4689
+ traverse5__default.default(ast, {
3257
4690
  JSXElement: (nodePath) => {
3258
4691
  const element = nodePath.node;
3259
4692
  const name = getJsxElementName(element.openingElement);
@@ -3484,7 +4917,7 @@ var WebScreenAnalyzer = class {
3484
4917
  for (const call of extractWebNavigationCalls(ast)) {
3485
4918
  if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
3486
4919
  }
3487
- traverse4__default.default(ast, {
4920
+ traverse5__default.default(ast, {
3488
4921
  JSXOpeningElement: (nodePath) => {
3489
4922
  const element = nodePath.node;
3490
4923
  const name = getJsxElementName(element);
@@ -3504,7 +4937,7 @@ var WebScreenAnalyzer = class {
3504
4937
  extractCollections(ast) {
3505
4938
  const collections = [];
3506
4939
  const seen = /* @__PURE__ */ new Set();
3507
- traverse4__default.default(ast, {
4940
+ traverse5__default.default(ast, {
3508
4941
  JSXExpressionContainer: (nodePath) => {
3509
4942
  const expr = nodePath.node.expression;
3510
4943
  if (!BabelTypes__namespace.isCallExpression(expr) || !BabelTypes__namespace.isMemberExpression(expr.callee) || !BabelTypes__namespace.isIdentifier(expr.callee.object) || !BabelTypes__namespace.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
@@ -3616,7 +5049,7 @@ function firstCalledFunctionName(node) {
3616
5049
  }
3617
5050
  function containsWindowConfirm(body) {
3618
5051
  let found = false;
3619
- traverse4__default.default(
5052
+ traverse5__default.default(
3620
5053
  body,
3621
5054
  {
3622
5055
  noScope: true,
@@ -3668,7 +5101,7 @@ function collectionItemNames(callback) {
3668
5101
  function collectionDisplayFields(callback, itemNames) {
3669
5102
  const fields = /* @__PURE__ */ new Set();
3670
5103
  if (!callback.body) return [];
3671
- traverse4__default.default(
5104
+ traverse5__default.default(
3672
5105
  callback.body,
3673
5106
  {
3674
5107
  noScope: true,
@@ -3685,7 +5118,7 @@ function collectionDisplayFields(callback, itemNames) {
3685
5118
  function collectionKeyField(callback, itemNames) {
3686
5119
  let keyField;
3687
5120
  if (!callback.body) return void 0;
3688
- traverse4__default.default(
5121
+ traverse5__default.default(
3689
5122
  callback.body,
3690
5123
  {
3691
5124
  noScope: true,
@@ -3767,7 +5200,9 @@ var WebNavigationAnalyzer = class {
3767
5200
  for (const filePath of files) {
3768
5201
  try {
3769
5202
  const content = await fs$1.promises.readFile(filePath, "utf-8");
3770
- if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
5203
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
5204
+ content
5205
+ )) {
3771
5206
  continue;
3772
5207
  }
3773
5208
  const ast = parseSource(content, this.config.parserPlugins);
@@ -3797,8 +5232,8 @@ var WebNavigationAnalyzer = class {
3797
5232
  ...this.config.exclude || [],
3798
5233
  ...this.navigationExclude
3799
5234
  ];
3800
- const files = await glob__default.default(patterns, { cwd: this.config.rootDir, ignore });
3801
- return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
5235
+ const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
5236
+ return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
3802
5237
  }
3803
5238
  // ── JSX <Route> style ────────────────────────────────────────────
3804
5239
  extractJsxRoutes(ast) {
@@ -3826,7 +5261,7 @@ var WebNavigationAnalyzer = class {
3826
5261
  if (BabelTypes__namespace.isJSXElement(child)) visitRoute(child, fullPath);
3827
5262
  }
3828
5263
  };
3829
- traverse4__default.default(ast, {
5264
+ traverse5__default.default(ast, {
3830
5265
  JSXElement: (nodePath) => {
3831
5266
  const name = getJsxElementName(nodePath.node.openingElement);
3832
5267
  if (name !== "Routes" && name !== "Route") return;
@@ -3865,7 +5300,7 @@ var WebNavigationAnalyzer = class {
3865
5300
  "createMemoryRouter",
3866
5301
  "useRoutes"
3867
5302
  ]);
3868
- traverse4__default.default(ast, {
5303
+ traverse5__default.default(ast, {
3869
5304
  CallExpression: (nodePath) => {
3870
5305
  const callee = nodePath.node.callee;
3871
5306
  if (!BabelTypes__namespace.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
@@ -3970,11 +5405,13 @@ var WebNavigationAnalyzer = class {
3970
5405
  return {
3971
5406
  screens,
3972
5407
  initialScreen: initialRoute?.screenName ?? "",
3973
- navigators: routes.length > 0 ? [{
3974
- name: navigatorName,
3975
- type: WEB_NAVIGATOR_TYPE,
3976
- screens: screenNames
3977
- }] : []
5408
+ navigators: routes.length > 0 ? [
5409
+ {
5410
+ name: navigatorName,
5411
+ type: WEB_NAVIGATOR_TYPE,
5412
+ screens: screenNames
5413
+ }
5414
+ ] : []
3978
5415
  };
3979
5416
  }
3980
5417
  };
@@ -4296,8 +5733,8 @@ var ZodParsedType = util.arrayToEnum([
4296
5733
  "set"
4297
5734
  ]);
4298
5735
  var getParsedType = (data) => {
4299
- const t12 = typeof data;
4300
- switch (t12) {
5736
+ const t15 = typeof data;
5737
+ switch (t15) {
4301
5738
  case "undefined":
4302
5739
  return ZodParsedType.undefined;
4303
5740
  case "string":
@@ -4569,8 +6006,8 @@ function getErrorMap() {
4569
6006
 
4570
6007
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4571
6008
  var makeIssue = (params) => {
4572
- const { data, path: path9, errorMaps, issueData } = params;
4573
- const fullPath = [...path9, ...issueData.path || []];
6009
+ const { data, path: path11, errorMaps, issueData } = params;
6010
+ const fullPath = [...path11, ...issueData.path || []];
4574
6011
  const fullIssue = {
4575
6012
  ...issueData,
4576
6013
  path: fullPath
@@ -4686,11 +6123,11 @@ var errorUtil;
4686
6123
 
4687
6124
  // ../../node_modules/zod/v3/types.js
4688
6125
  var ParseInputLazyPath = class {
4689
- constructor(parent, value, path9, key) {
6126
+ constructor(parent, value, path11, key) {
4690
6127
  this._cachedPath = [];
4691
6128
  this.parent = parent;
4692
6129
  this.data = value;
4693
- this._path = path9;
6130
+ this._path = path11;
4694
6131
  this._key = key;
4695
6132
  }
4696
6133
  get path() {
@@ -8131,7 +9568,36 @@ var coerce = {
8131
9568
  };
8132
9569
  var NEVER = INVALID;
8133
9570
 
8134
- // ../shared/dist/chunk-MKT45LRA.mjs
9571
+ // ../shared/dist/chunk-SWQOZWHP.mjs
9572
+ var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
9573
+ var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
9574
+ function isSafeImageUrl(value) {
9575
+ if (value === null || value === void 0) return true;
9576
+ const trimmed = value.trim();
9577
+ if (trimmed === "") return true;
9578
+ if (!HAS_SCHEME_RE.test(trimmed)) return true;
9579
+ return SAFE_IMAGE_URL_RE.test(trimmed);
9580
+ }
9581
+
9582
+ // ../shared/dist/chunk-KUMPS6RH.mjs
9583
+ var SUPPORTED_APPILOTS_LOCALES = ["pt-BR", "en", "es", "fr"];
9584
+
9585
+ // ../shared/dist/chunk-2GTRKNPJ.mjs
9586
+ var symbols = external_exports.array(external_exports.string().max(240)).max(12);
9587
+ var controlEvidenceSchema = external_exports.object({
9588
+ version: external_exports.literal(1),
9589
+ siteId: external_exports.string().regex(/^[a-f0-9]{20}$/),
9590
+ component: external_exports.string().max(240),
9591
+ icons: symbols,
9592
+ handler: external_exports.string().max(240).optional(),
9593
+ calls: symbols,
9594
+ argumentBindings: symbols,
9595
+ conditions: symbols,
9596
+ nativeConfirmation: external_exports.object({
9597
+ title: external_exports.string().max(240).optional(),
9598
+ destructiveOption: external_exports.boolean()
9599
+ }).optional()
9600
+ });
8135
9601
  var locatorSourceSchema = external_exports.enum([
8136
9602
  "appilotsId",
8137
9603
  "testID",
@@ -8310,6 +9776,23 @@ var actionDescriptorSchema = external_exports.object({
8310
9776
  effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
8311
9777
  riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
8312
9778
  requiresConfirmation: external_exports.boolean().optional(),
9779
+ /**
9780
+ * How long this action's work is expected to take, in milliseconds,
9781
+ * DECLARED by the app — not inferred (that is `appilotsInferred`).
9782
+ *
9783
+ * The SDK's post-action wait was a constant: 6s, or 10s when the
9784
+ * generator could prove the handler awaits something. No constant
9785
+ * fits, because app operations run from ~100ms to minutes, and the
9786
+ * failure is silent in both directions — too short and the agent
9787
+ * photographs a loading screen with no controls on it, too long and
9788
+ * every fast press pays for the slowest one. Only the app knows.
9789
+ *
9790
+ * Absent means absent: the SDK keeps its current defaults, so a
9791
+ * document generated before this field existed behaves exactly as it
9792
+ * did. The SDK also clamps the value to its own safety ceiling — a
9793
+ * declared budget is a request, not a licence to hang the session.
9794
+ */
9795
+ asyncBudgetMs: external_exports.number().int().positive().optional(),
8313
9796
  appilotsInferred: appilotsInferredActionSchema.optional()
8314
9797
  }).passthrough();
8315
9798
  var screenPermissionDescriptorSchema = external_exports.object({
@@ -8492,7 +9975,7 @@ external_exports.object({
8492
9975
  version: external_exports.string().default("1.0"),
8493
9976
  content: external_exports.record(external_exports.unknown())
8494
9977
  });
8495
- var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
9978
+ var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
8496
9979
  var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
8497
9980
  external_exports.object({
8498
9981
  name: external_exports.string().min(1).max(100),
@@ -8503,8 +9986,8 @@ external_exports.object({
8503
9986
  scope: apiKeyScopeSchema.default("sdk"),
8504
9987
  environment: apiKeyEnvironmentSchema.optional(),
8505
9988
  expiresAt: external_exports.string().datetime().optional()
8506
- }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
8507
- message: 'projectId is required for scope "sdk"',
9989
+ }).refine((v) => v.scope === "operator" || !!v.projectId, {
9990
+ message: "projectId is required for SDK and publishing keys",
8508
9991
  path: ["projectId"]
8509
9992
  });
8510
9993
  var boundedString = (max) => external_exports.string().max(max);
@@ -8529,10 +10012,21 @@ var snapshotInputSchema = external_exports.object({
8529
10012
  type: boundedString(40).optional(),
8530
10013
  required: external_exports.boolean().optional(),
8531
10014
  invalid: external_exports.boolean().optional(),
10015
+ /**
10016
+ * Whether the field holds anything, as its own fact rather than an
10017
+ * inference over `value`. The value is the user's and is the first
10018
+ * thing a privacy policy withholds; the existence of a value is the
10019
+ * agent's and is what stops it filling the same field twice.
10020
+ */
10021
+ filled: external_exports.boolean().optional(),
10022
+ /** This input has keyboard focus right now. */
10023
+ focused: external_exports.boolean().optional(),
8532
10024
  inModal: external_exports.boolean().optional()
8533
10025
  }).passthrough();
8534
10026
  var snapshotButtonSchema = external_exports.object({
10027
+ controlEvidence: controlEvidenceSchema.optional(),
8535
10028
  id: boundedString(160).optional(),
10029
+ dispatchable: external_exports.boolean().optional(),
8536
10030
  provenance: identityProvenanceSchema.optional(),
8537
10031
  /**
8538
10032
  * False when the control is mounted but currently OUTSIDE the window —
@@ -8593,7 +10087,36 @@ var snapshotListSchema = external_exports.object({
8593
10087
  */
8594
10088
  source: boundedString(40).optional(),
8595
10089
  itemCount: external_exports.number().int().optional(),
10090
+ /**
10091
+ * Size of the whole collection when the app declared it, for a list
10092
+ * that is a WINDOW onto more data than it holds.
10093
+ *
10094
+ * `itemCount` is how many rows the list is rendering from and
10095
+ * `visibleItemCount` how many of those are mounted; neither can
10096
+ * express "there are 36 and you are looking at the first 20",
10097
+ * because a paginated list's `data` is the page. Absent means
10098
+ * unknown — never "same as itemCount".
10099
+ */
10100
+ totalItemCount: external_exports.number().int().optional(),
8596
10101
  visibleItemCount: external_exports.number().int().optional(),
10102
+ viewportItemCount: external_exports.number().int().nonnegative().optional(),
10103
+ exploration: external_exports.object({
10104
+ revision: external_exports.number().int().nonnegative(),
10105
+ observedItemCount: external_exports.number().int().min(0).max(5e3),
10106
+ observedRanges: external_exports.array(
10107
+ external_exports.object({
10108
+ start: external_exports.number().int().nonnegative(),
10109
+ end: external_exports.number().int().nonnegative()
10110
+ })
10111
+ ).max(32),
10112
+ rangesTruncated: external_exports.boolean(),
10113
+ coverage: external_exports.enum(["partial", "all-loaded"]),
10114
+ pagination: external_exports.enum(["possible", "not-declared"]),
10115
+ scrollSteps: external_exports.number().int().nonnegative(),
10116
+ remainingScrollSteps: external_exports.number().int().nonnegative(),
10117
+ consecutiveNoProgress: external_exports.number().int().nonnegative(),
10118
+ lastScroll: external_exports.enum(["moved", "no-progress", "boundary", "unverified"]).optional()
10119
+ }).optional(),
8597
10120
  refreshing: external_exports.boolean().optional(),
8598
10121
  empty: external_exports.boolean().optional(),
8599
10122
  label: boundedString(300).optional(),
@@ -8641,6 +10164,7 @@ var snapshotChoiceGroupSchema = external_exports.object({
8641
10164
  }).passthrough();
8642
10165
  var snapshotElementSchema = external_exports.object({
8643
10166
  id: boundedString(160).optional(),
10167
+ dispatchable: external_exports.boolean().optional(),
8644
10168
  role: boundedString(40).optional(),
8645
10169
  label: boundedString(300).optional(),
8646
10170
  texts: external_exports.array(boundedString(500)).max(50).optional(),
@@ -8684,6 +10208,34 @@ var agentSnapshotSchema = external_exports.object({
8684
10208
  * it. Optional: older SDKs never clamp and never send it.
8685
10209
  */
8686
10210
  truncated: external_exports.boolean().optional(),
10211
+ /**
10212
+ * What moved since the previous observation, computed on the device
10213
+ * because that is the only side holding both snapshots.
10214
+ *
10215
+ * Shape only — counts, booleans, and ids the app declared itself — so
10216
+ * it survives an observation whose content was withheld. ABSENT means
10217
+ * there was no previous observation to compare against; `unchanged:
10218
+ * true` means we compared and nothing moved, which is the strongest
10219
+ * evidence available that an action did nothing. The two must not be
10220
+ * collapsed, for the same reason `truncated` exists.
10221
+ */
10222
+ delta: external_exports.object({
10223
+ routeChanged: external_exports.boolean().optional(),
10224
+ modalOpened: external_exports.boolean().optional(),
10225
+ modalClosed: external_exports.boolean().optional(),
10226
+ loadingStarted: external_exports.boolean().optional(),
10227
+ loadingFinished: external_exports.boolean().optional(),
10228
+ textsAdded: external_exports.number().int().nonnegative().optional(),
10229
+ textsRemoved: external_exports.number().int().nonnegative().optional(),
10230
+ buttonsAddedIndices: external_exports.array(external_exports.number().int().nonnegative()).max(12).optional(),
10231
+ buttonsRemoved: external_exports.number().int().nonnegative().optional(),
10232
+ visibleRowsDelta: external_exports.number().int().optional(),
10233
+ totalRowsDelta: external_exports.number().int().optional(),
10234
+ fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
10235
+ fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
10236
+ invalidAppeared: external_exports.boolean().optional(),
10237
+ unchanged: external_exports.boolean().optional()
10238
+ }).passthrough().optional(),
8687
10239
  /**
8688
10240
  * How many of this screen's controls the client could name, split by
8689
10241
  * `identityProvenance`. Diagnostic only — the relay never grounds an
@@ -8702,6 +10254,10 @@ var agentSnapshotSchema = external_exports.object({
8702
10254
  }).passthrough().optional()
8703
10255
  }).passthrough();
8704
10256
  var agentContextSchema = external_exports.object({
10257
+ missionProtocol: external_exports.literal(1).optional(),
10258
+ missionId: boundedString(160).optional(),
10259
+ /** Preferred supported device language, reported by the SDK independently of map/UI labels. */
10260
+ deviceLocale: external_exports.enum(SUPPORTED_APPILOTS_LOCALES).optional(),
8705
10261
  /**
8706
10262
  * Client platform this observation was captured on. Optional and
8707
10263
  * additive (see `clientPlatformSchema`) — absent means
@@ -8717,7 +10273,19 @@ var agentContextSchema = external_exports.object({
8717
10273
  rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8718
10274
  currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8719
10275
  routeNames: external_exports.array(boundedString(200)).max(500).optional(),
8720
- canGoBack: external_exports.boolean().optional()
10276
+ canGoBack: external_exports.boolean().optional(),
10277
+ /**
10278
+ * The stack a back press pops through, oldest first, ending on
10279
+ * the current screen. `canGoBack` says a back exists; this says
10280
+ * where it goes.
10281
+ */
10282
+ backStack: external_exports.array(boundedString(200)).max(50).optional(),
10283
+ /**
10284
+ * Screens the session has been on, oldest first. Names only —
10285
+ * params carry record ids and often personal data, and a route
10286
+ * name is structure.
10287
+ */
10288
+ visited: external_exports.array(boundedString(200)).max(8).optional()
8721
10289
  }).passthrough().optional(),
8722
10290
  screenMetadata: external_exports.object({
8723
10291
  name: boundedString(200).optional(),
@@ -8823,7 +10391,7 @@ var formFillPayloadSchema = external_exports.object({
8823
10391
  submitAfterFill: external_exports.boolean().optional()
8824
10392
  }).passthrough();
8825
10393
  var uiInteractionPayloadSchema = external_exports.object({
8826
- action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
10394
+ action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
8827
10395
  // `targetId` is the canonical server/LLM field. SDK runtimes still
8828
10396
  // accept `componentId` as a compatibility alias.
8829
10397
  targetId: external_exports.string().min(1),
@@ -8940,6 +10508,12 @@ var actionDiagnoseSchema = external_exports.object({
8940
10508
  requiresUserInput: external_exports.boolean().optional()
8941
10509
  });
8942
10510
  var actionResultSchema = external_exports.object({
10511
+ nativeConfirmation: external_exports.object({
10512
+ title: external_exports.string().max(300),
10513
+ message: external_exports.string().max(1e3).optional(),
10514
+ buttonLabel: external_exports.string().max(160).optional(),
10515
+ handlerCompleted: external_exports.boolean()
10516
+ }).optional(),
8943
10517
  actionId: external_exports.string(),
8944
10518
  type: external_exports.string(),
8945
10519
  success: external_exports.boolean(),
@@ -8991,8 +10565,15 @@ external_exports.object({
8991
10565
  // issue #169 — the observation the server grounds targets against, now
8992
10566
  // validated + bounded at the border instead of z.record(z.unknown()).
8993
10567
  context: agentContextSchema.optional(),
8994
- /** Hop counter — server enforces a cap to prevent runaway loops */
8995
- hop: external_exports.number().int().min(1).max(10).optional()
10568
+ /**
10569
+ * Hop counter — server enforces a cap to prevent runaway loops.
10570
+ * The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
10571
+ * #503, apps/api/src/modules/agents/routes.ts): the first over-budget
10572
+ * hop has to get through validation so the route can answer it with
10573
+ * the friendly automation-limit message instead of a 422. The +3
10574
+ * headroom mirrors what 7/10 was before the recalibration.
10575
+ */
10576
+ hop: external_exports.number().int().min(1).max(13).optional()
8996
10577
  });
8997
10578
  var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
8998
10579
  var screenPermissionSchema = external_exports.object({
@@ -9060,7 +10641,7 @@ external_exports.object({
9060
10641
  /** null = remove webhook, undefined = leave unchanged. */
9061
10642
  budgetWebhookUrl: external_exports.string().url().nullable().optional()
9062
10643
  }).strict();
9063
- var localeSchema = external_exports.enum(["pt-BR", "en", "es"]);
10644
+ var localeSchema = external_exports.enum(SUPPORTED_APPILOTS_LOCALES);
9064
10645
  var hexColorSchema = external_exports.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
9065
10646
  message: "Must be a hex color like #6366f1"
9066
10647
  });
@@ -9098,15 +10679,18 @@ var themeTokensSchema = external_exports.object({
9098
10679
  */
9099
10680
  mode: external_exports.enum(["auto", "light", "dark"]).optional()
9100
10681
  }).strict();
10682
+ var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
10683
+ message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
10684
+ });
9101
10685
  external_exports.object({
9102
10686
  /** Tone / persona instructions appended to the system prompt. */
9103
10687
  personaPrompt: external_exports.string().max(2048).nullable(),
9104
10688
  /** Display name in the chat header (e.g. "Aria"). */
9105
10689
  assistantName: external_exports.string().max(60).nullable(),
9106
10690
  /** URL or remote asset id for the avatar shown next to assistant turns. */
9107
- assistantAvatar: external_exports.string().max(500).nullable(),
10691
+ assistantAvatar: imageSourceField.nullable(),
9108
10692
  /** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
9109
- emptyStateIcon: external_exports.string().max(500).nullable(),
10693
+ emptyStateIcon: imageSourceField.nullable(),
9110
10694
  /** First-message text shown in the empty state. */
9111
10695
  welcomeMessage: external_exports.string().max(500).nullable(),
9112
10696
  /** Title rendered at the top of the chat. */
@@ -9120,20 +10704,20 @@ external_exports.object({
9120
10704
  /** Chat-open FAB background color. Null falls back to theme.colors.primary. */
9121
10705
  triggerButtonColor: hexColorSchema.nullable(),
9122
10706
  /** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
9123
- triggerButtonImageUrl: external_exports.string().max(500).nullable()
10707
+ triggerButtonImageUrl: imageSourceField.nullable()
9124
10708
  });
9125
10709
  external_exports.object({
9126
10710
  personaPrompt: external_exports.string().max(2048).nullable().optional(),
9127
10711
  assistantName: external_exports.string().max(60).nullable().optional(),
9128
- assistantAvatar: external_exports.string().max(500).nullable().optional(),
9129
- emptyStateIcon: external_exports.string().max(500).nullable().optional(),
10712
+ assistantAvatar: imageSourceField.nullable().optional(),
10713
+ emptyStateIcon: imageSourceField.nullable().optional(),
9130
10714
  welcomeMessage: external_exports.string().max(500).nullable().optional(),
9131
10715
  chatTitle: external_exports.string().max(60).nullable().optional(),
9132
10716
  poweredByVisible: external_exports.boolean().optional(),
9133
10717
  theme: themeTokensSchema.nullable().optional(),
9134
10718
  defaultLocale: localeSchema.nullable().optional(),
9135
10719
  triggerButtonColor: hexColorSchema.nullable().optional(),
9136
- triggerButtonImageUrl: external_exports.string().max(500).nullable().optional()
10720
+ triggerButtonImageUrl: imageSourceField.nullable().optional()
9137
10721
  }).strict();
9138
10722
  var sandboxObservationSchema = external_exports.object({
9139
10723
  route: external_exports.string().max(200).optional(),
@@ -9153,7 +10737,19 @@ var sandboxObservationSchema = external_exports.object({
9153
10737
  type: external_exports.string().max(40).optional(),
9154
10738
  placeholder: external_exports.string().max(200).optional(),
9155
10739
  /** Inline validation state (SDK snapshot field). */
9156
- invalid: external_exports.boolean().optional()
10740
+ invalid: external_exports.boolean().optional(),
10741
+ /**
10742
+ * Whether the field holds anything, as its own fact rather than
10743
+ * an inference over `value`.
10744
+ *
10745
+ * Here because the eval posts through this schema: a fact the
10746
+ * SDK produces and this contract has no word for is a fact the
10747
+ * corpus can never grade the agent on. `invalid` and `required`
10748
+ * were already here; these two were the half that was missing.
10749
+ */
10750
+ filled: external_exports.boolean().optional(),
10751
+ /** This input has keyboard focus right now. */
10752
+ focused: external_exports.boolean().optional()
9157
10753
  })
9158
10754
  ).max(100).optional(),
9159
10755
  buttons: external_exports.array(
@@ -9632,7 +11228,7 @@ var manifestSchema = external_exports.object({
9632
11228
  navigation: navigationGraphSchema.partial().optional()
9633
11229
  }).passthrough();
9634
11230
  async function loadManifest(rootDir, manifestPath) {
9635
- const resolvedPath = path8__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
11231
+ const resolvedPath = path2__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
9636
11232
  let raw;
9637
11233
  try {
9638
11234
  raw = await fs.readFile(resolvedPath, "utf-8");
@@ -9715,6 +11311,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
9715
11311
  };
9716
11312
  }
9717
11313
 
11314
+ // src/generators/checksum.ts
11315
+ function serializeForChecksum(document) {
11316
+ const { generatedAt: _generatedAt, ...content } = document;
11317
+ return JSON.stringify(content, null, 2);
11318
+ }
11319
+
9718
11320
  // src/pipeline/enrichment.ts
9719
11321
  function enrichScreenForAgent(screen) {
9720
11322
  const targets = mergeTargets([
@@ -9791,7 +11393,10 @@ function mergeTargets(targets) {
9791
11393
  for (const target of targets) {
9792
11394
  if (!target.id) continue;
9793
11395
  const existing = byId.get(target.id);
9794
- byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
11396
+ byId.set(
11397
+ target.id,
11398
+ existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
11399
+ );
9795
11400
  }
9796
11401
  return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
9797
11402
  }
@@ -9836,7 +11441,10 @@ function synthesizeFlows(screen, targets) {
9836
11441
  intent: "destructive_action",
9837
11442
  steps: [
9838
11443
  { type: "press", target: action.id, label: action.label },
9839
- { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
11444
+ {
11445
+ type: "confirm",
11446
+ description: "Wait for native or custom confirmation before continuing"
11447
+ },
9840
11448
  { type: "wait", description: describeWait(action) }
9841
11449
  ],
9842
11450
  waitPolicy: waitPolicyForAction(action),
@@ -9851,10 +11459,19 @@ function synthesizeFlows(screen, targets) {
9851
11459
  title: `Act on an item in ${collection.id}`,
9852
11460
  intent: "list_action",
9853
11461
  steps: [
9854
- { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
9855
- { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
11462
+ {
11463
+ type: "choose-list-item",
11464
+ target: collection.id,
11465
+ description: "Resolve the user reference to a visible or searchable row"
11466
+ },
11467
+ {
11468
+ type: "press",
11469
+ description: collection.rowAction?.description ?? "Open the row action"
11470
+ }
9856
11471
  ],
9857
- waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
11472
+ waitPolicy: {
11473
+ expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
11474
+ }
9858
11475
  });
9859
11476
  }
9860
11477
  }
@@ -9862,25 +11479,32 @@ function synthesizeFlows(screen, targets) {
9862
11479
  }
9863
11480
  function waitPolicyForAction(action) {
9864
11481
  const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
9865
- const signals = [action.successSignal, action.failureSignal].filter(Boolean);
11482
+ const signals = [action.successSignal, action.failureSignal].filter(
11483
+ Boolean
11484
+ );
11485
+ const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
9866
11486
  return {
9867
11487
  ...expectedOutcome ? { expectedOutcome } : {},
9868
11488
  ...signals && signals.length > 0 ? { signals } : {},
9869
- ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
11489
+ ...maxMs !== void 0 ? { maxMs } : {}
9870
11490
  };
9871
11491
  }
9872
11492
  function describeWait(action) {
9873
11493
  if (action.successSignal?.description) return action.successSignal.description;
9874
- if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
11494
+ if (action.successSignal?.type === "goBack")
11495
+ return "Wait for the app to return to the previous screen";
9875
11496
  if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
9876
- if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
11497
+ if (action.appilotsInferred?.expectedOutcome)
11498
+ return `Wait for ${action.appilotsInferred.expectedOutcome}`;
9877
11499
  return "Wait for the UI to settle";
9878
11500
  }
9879
11501
  function synthesizeAgentHints(screen, targets, flows) {
9880
11502
  const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
9881
11503
  const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
9882
11504
  const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
9883
- const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
11505
+ const firstAsyncAction = screen.actions.find(
11506
+ (action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
11507
+ );
9884
11508
  const hints = {
9885
11509
  primaryGoal: synthesizePrimaryGoal(screen),
9886
11510
  commonTasks,
@@ -9907,7 +11531,9 @@ function synthesizePrimaryGoal(screen) {
9907
11531
  }
9908
11532
  const fieldCount = uniqueFields.size;
9909
11533
  if (fieldCount > 0) {
9910
- const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
11534
+ const requiredCount = Array.from(uniqueFields.values()).filter(
11535
+ (field) => field.required
11536
+ ).length;
9911
11537
  const fieldLabel = fieldCount === 1 ? "field" : "fields";
9912
11538
  goals.push(
9913
11539
  requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
@@ -9921,7 +11547,9 @@ function synthesizePrimaryGoal(screen) {
9921
11547
  if (submitCount > 0) {
9922
11548
  goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
9923
11549
  }
9924
- const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
11550
+ const asyncCount = screen.actions.filter(
11551
+ (action) => action.appilotsInferred?.isAsyncTrigger
11552
+ ).length;
9925
11553
  if (asyncCount > 0) {
9926
11554
  goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
9927
11555
  }
@@ -9996,7 +11624,7 @@ var MCPGenerator = class _MCPGenerator {
9996
11624
  const agentReadyScreens = mergedScreens.map(
9997
11625
  (screen) => enrichScreenForAgent({
9998
11626
  ...screen,
9999
- filePath: screen.filePath ? path8__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
11627
+ filePath: screen.filePath ? path2__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
10000
11628
  })
10001
11629
  );
10002
11630
  const projectInfo = await this.getProjectInfo();
@@ -10020,19 +11648,33 @@ var MCPGenerator = class _MCPGenerator {
10020
11648
  }
10021
11649
  };
10022
11650
  const serialized = JSON.stringify(document, null, 2);
10023
- const checksum = this.calculateChecksum(serialized);
10024
- const filePath = path8__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
11651
+ const checksum = this.calculateChecksum(serializeForChecksum(document));
11652
+ const filePath = path2__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
10025
11653
  await fs.writeFile(filePath, serialized, "utf-8");
10026
11654
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10027
- const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
11655
+ const controlFiles = analyzed.controlEvidenceFiles ?? {};
11656
+ await fs.writeFile(
11657
+ path2__namespace.default.resolve(outputDir, "control-evidence.json"),
11658
+ JSON.stringify({ version: 1, files: controlFiles }, null, 2),
11659
+ "utf-8"
11660
+ );
11661
+ const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
10028
11662
  await fs.writeFile(checksumFilePath, checksum, "utf-8");
10029
11663
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
11664
+ const evidenceCount = Object.values(controlFiles).reduce(
11665
+ (sum, entries) => sum + entries.length,
11666
+ 0
11667
+ );
11668
+ console.log(
11669
+ `[MCPGenerator] Source evidence: ${evidenceCount} icon controls across ${Object.keys(controlFiles).length} files (runtime binding required)`
11670
+ );
10030
11671
  console.log("[MCPGenerator] Generation complete!");
10031
11672
  return {
10032
11673
  document,
10033
11674
  filePath,
10034
11675
  format: this.options.format,
10035
- checksum
11676
+ checksum,
11677
+ ...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
10036
11678
  };
10037
11679
  }
10038
11680
  /**
@@ -10044,7 +11686,7 @@ var MCPGenerator = class _MCPGenerator {
10044
11686
  * after calling `generate()`.
10045
11687
  */
10046
11688
  static async readPreviousChecksum(outputDir) {
10047
- const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
11689
+ const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
10048
11690
  try {
10049
11691
  const content = await fs.readFile(checksumFilePath, "utf-8");
10050
11692
  return content.trim() || null;
@@ -10063,7 +11705,7 @@ var MCPGenerator = class _MCPGenerator {
10063
11705
  */
10064
11706
  async getProjectInfo() {
10065
11707
  try {
10066
- const packageJsonPath = path8__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
11708
+ const packageJsonPath = path2__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
10067
11709
  const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
10068
11710
  const packageJson = JSON.parse(packageJsonContent);
10069
11711
  return {
@@ -10091,7 +11733,8 @@ var KNOWN_CONFIG_KEYS = [
10091
11733
  "navigationExclude",
10092
11734
  "platform",
10093
11735
  "manifestPath",
10094
- "eval"
11736
+ "eval",
11737
+ "knowledge"
10095
11738
  ];
10096
11739
  var KEY_ALIASES = {
10097
11740
  apiUrl: "serverUrl",
@@ -10146,12 +11789,12 @@ function getEnvOverrides(env = process.env) {
10146
11789
  return trimmed ? trimmed : void 0;
10147
11790
  };
10148
11791
  return {
10149
- apiKey: clean(env.APPILOTS_API_KEY),
11792
+ apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
10150
11793
  projectId: clean(env.APPILOTS_PROJECT_ID),
10151
11794
  serverUrl: clean(env.APPILOTS_SERVER_URL)
10152
11795
  };
10153
11796
  }
10154
- function loadConfig(onWarn) {
11797
+ function loadConfig(onWarn, options = {}) {
10155
11798
  const configPath = getConfigPath();
10156
11799
  const env = getEnvOverrides();
10157
11800
  let fileConfig = {};
@@ -10190,6 +11833,10 @@ function loadConfig(onWarn) {
10190
11833
  };
10191
11834
  const validation = validateConfig(merged);
10192
11835
  validation.warnings.unshift(...legacyWarnings);
11836
+ if (options.requireApiKey === false) {
11837
+ validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
11838
+ validation.valid = validation.errors.length === 0;
11839
+ }
10193
11840
  if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
10194
11841
  for (const key of Object.keys(fileConfig)) {
10195
11842
  if (KNOWN_CONFIG_KEYS.includes(key)) continue;
@@ -10219,7 +11866,7 @@ function loadConfig(onWarn) {
10219
11866
  return merged;
10220
11867
  }
10221
11868
  function saveConfig(dir, config) {
10222
- const configPath = path8.join(dir, ".appilotsrc");
11869
+ const configPath = path2.join(dir, ".appilotsrc");
10223
11870
  const existingConfig = getConfigPath() ? loadConfig() : null;
10224
11871
  const mergedConfig = {
10225
11872
  apiKey: config.apiKey || existingConfig?.apiKey || "",
@@ -10235,7 +11882,8 @@ function saveConfig(dir, config) {
10235
11882
  navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
10236
11883
  platform: config.platform || existingConfig?.platform,
10237
11884
  manifestPath: config.manifestPath || existingConfig?.manifestPath,
10238
- eval: config.eval || existingConfig?.eval
11885
+ eval: config.eval || existingConfig?.eval,
11886
+ knowledge: config.knowledge || existingConfig?.knowledge
10239
11887
  };
10240
11888
  try {
10241
11889
  fs$1.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
@@ -10246,19 +11894,19 @@ function saveConfig(dir, config) {
10246
11894
  }
10247
11895
  }
10248
11896
  function getConfigPath() {
10249
- let currentDir = path8.resolve(process.cwd());
10250
- const root = path8.resolve("/");
11897
+ let currentDir = path2.resolve(process.cwd());
11898
+ const root = path2.resolve("/");
10251
11899
  while (currentDir !== root) {
10252
- const configPath = path8.join(currentDir, ".appilotsrc");
11900
+ const configPath = path2.join(currentDir, ".appilotsrc");
10253
11901
  try {
10254
11902
  if (fs$1.existsSync(configPath) && fs$1.statSync(configPath).isFile()) {
10255
11903
  return configPath;
10256
11904
  }
10257
11905
  } catch {
10258
11906
  }
10259
- currentDir = path8.resolve(currentDir, "..");
11907
+ currentDir = path2.resolve(currentDir, "..");
10260
11908
  }
10261
- const rootConfigPath = path8.join(root, ".appilotsrc");
11909
+ const rootConfigPath = path2.join(root, ".appilotsrc");
10262
11910
  try {
10263
11911
  if (fs$1.existsSync(rootConfigPath) && fs$1.statSync(rootConfigPath).isFile()) {
10264
11912
  return rootConfigPath;
@@ -10328,6 +11976,19 @@ function validateConfig(config) {
10328
11976
  if (config.manifestPath !== void 0 && typeof config.manifestPath !== "string") {
10329
11977
  errors.push("manifestPath must be a string");
10330
11978
  }
11979
+ if (config.knowledge !== void 0) {
11980
+ if (typeof config.knowledge !== "object" || config.knowledge === null || Array.isArray(config.knowledge)) {
11981
+ errors.push("knowledge must be an object");
11982
+ } else {
11983
+ const kc = config.knowledge;
11984
+ for (const field of ["sources", "exclude"]) {
11985
+ const value = kc[field];
11986
+ if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
11987
+ errors.push(`knowledge.${field} must be an array of strings`);
11988
+ }
11989
+ }
11990
+ }
11991
+ }
10331
11992
  if (config.eval !== void 0) {
10332
11993
  if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
10333
11994
  errors.push("eval must be an object");
@@ -10604,6 +12265,48 @@ var AppilotsAPIClient = class {
10604
12265
  };
10605
12266
  }
10606
12267
  }
12268
+ /**
12269
+ * Syncs knowledge documents with the Appilots backend.
12270
+ *
12271
+ * @param documents Array of documents to sync (filename, base64 content, mimeType, checksum)
12272
+ * @returns KnowledgeSyncResult with counts of uploaded, skipped, and errored docs
12273
+ */
12274
+ async knowledgeSync(documents) {
12275
+ try {
12276
+ const response = await this.request(`${this.baseUrl}/cli/knowledge/sync`, {
12277
+ method: "POST",
12278
+ headers: {
12279
+ "Content-Type": "application/json",
12280
+ Authorization: `Bearer ${this.apiKey}`
12281
+ },
12282
+ body: JSON.stringify({ documents })
12283
+ });
12284
+ if (!response.ok) {
12285
+ const errorData = await response.json().catch(() => ({}));
12286
+ return {
12287
+ success: false,
12288
+ uploaded: 0,
12289
+ skipped: 0,
12290
+ errors: 0,
12291
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
12292
+ };
12293
+ }
12294
+ const json = await response.json();
12295
+ const inner = json.data ?? json;
12296
+ return {
12297
+ success: true,
12298
+ ...inner
12299
+ };
12300
+ } catch (error) {
12301
+ return {
12302
+ success: false,
12303
+ uploaded: 0,
12304
+ skipped: 0,
12305
+ errors: 0,
12306
+ error: error instanceof Error ? error.message : "Failed to sync knowledge with Appilots API"
12307
+ };
12308
+ }
12309
+ }
10607
12310
  /**
10608
12311
  * Checks if the Appilots API server is healthy
10609
12312
  *
@@ -10622,7 +12325,7 @@ var AppilotsAPIClient = class {
10622
12325
  };
10623
12326
 
10624
12327
  // src/version.ts
10625
- var CLI_VERSION = "0.11.0";
12328
+ var CLI_VERSION = "0.13.0";
10626
12329
 
10627
12330
  exports.AppilotsAPIClient = AppilotsAPIClient;
10628
12331
  exports.CLI_VERSION = CLI_VERSION;