@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.mjs CHANGED
@@ -1,19 +1,363 @@
1
1
  import fs, { readFile, mkdir, writeFile } from 'fs/promises';
2
- import * as path8 from 'path';
3
- import path8__default, { join, resolve } from 'path';
4
- import traverse4 from '@babel/traverse';
2
+ import { createHash } from 'crypto';
3
+ import traverse5 from '@babel/traverse';
5
4
  import * as BabelTypes from '@babel/types';
6
- import glob from 'fast-glob';
5
+ import * as path2 from 'path';
6
+ import path2__default, { join, resolve } from 'path';
7
+ import fastGlob from 'fast-glob';
7
8
  import * as parser from '@babel/parser';
8
9
  import { parse } from '@babel/parser';
9
- import { promises, readFileSync, writeFileSync, existsSync, statSync } from 'fs';
10
- import { createHash } from 'crypto';
10
+ import { promises, readFileSync, writeFileSync, existsSync, statSync, readdirSync } from 'fs';
11
11
 
12
12
  var __defProp = Object.defineProperty;
13
13
  var __export = (target, all) => {
14
14
  for (var name in all)
15
15
  __defProp(target, name, { get: all[name], enumerable: true });
16
16
  };
17
+ var MAX_HOPS = 8;
18
+ function unwrap(path11) {
19
+ while (path11.isTSAsExpression() || path11.isTSTypeAssertion() || path11.isTSNonNullExpression() || path11.isTSSatisfiesExpression() || path11.isParenthesizedExpression())
20
+ path11 = path11.get("expression");
21
+ return path11;
22
+ }
23
+ function constantValue(path11, depth = 0) {
24
+ if (depth > MAX_HOPS) return void 0;
25
+ path11 = unwrap(path11);
26
+ if (!path11.isIdentifier()) return path11;
27
+ const binding = path11.scope.getBinding(path11.node.name);
28
+ if (!binding?.constant || !binding.path.isVariableDeclarator()) return path11;
29
+ const init = binding.path.get("init");
30
+ const resolved = init.node ? constantValue(init, depth + 1) : void 0;
31
+ if (resolved?.isObjectExpression() && binding.referencePaths.some(
32
+ (reference) => !reference.parentPath?.isJSXSpreadAttribute() && !reference.parentPath?.isSpreadElement()
33
+ ))
34
+ return void 0;
35
+ return resolved;
36
+ }
37
+ function propValue(path11, name) {
38
+ const readObject = (path12) => {
39
+ const resolved = constantValue(path12);
40
+ if (!resolved?.isObjectExpression()) return { blocked: true };
41
+ for (const property of [...resolved.get("properties")].reverse()) {
42
+ if (property.isSpreadElement()) {
43
+ const found = readObjectBounded(property.get("argument"));
44
+ if (found.value || found.blocked) return found;
45
+ } else if (property.isObjectProperty() || property.isObjectMethod()) {
46
+ if (property.node.computed) return { blocked: true };
47
+ const key = property.node.key;
48
+ if ((BabelTypes.isIdentifier(key) ? key.name : BabelTypes.isStringLiteral(key) ? key.value : "") === name)
49
+ return property.isObjectProperty() ? { value: property.get("value") } : { value: property };
50
+ }
51
+ }
52
+ return {};
53
+ };
54
+ let objectBudget = MAX_HOPS;
55
+ const readObjectBounded = (path12) => objectBudget-- > 0 ? readObject(path12) : { blocked: true };
56
+ for (const attr of [...path11.get("attributes")].reverse()) {
57
+ if (attr.isJSXAttribute() && BabelTypes.isJSXIdentifier(attr.node.name, { name })) {
58
+ const value = attr.get("value");
59
+ return value.isJSXExpressionContainer() ? unwrap(value.get("expression")) : value.node ? value : void 0;
60
+ }
61
+ if (attr.isJSXSpreadAttribute()) {
62
+ const found = readObjectBounded(attr.get("argument"));
63
+ if (found.value || found.blocked) return found.value;
64
+ }
65
+ }
66
+ return void 0;
67
+ }
68
+ function importIdentity(path11, depth = 0) {
69
+ if (depth > MAX_HOPS) return void 0;
70
+ path11 = unwrap(path11);
71
+ if (path11.isIdentifier() || path11.isJSXIdentifier()) {
72
+ const binding = path11.scope.getBinding(path11.node.name);
73
+ if (!binding?.constant) return void 0;
74
+ const declaration = binding.path;
75
+ if (declaration.parentPath?.isImportDeclaration()) {
76
+ const module = declaration.parentPath.node.source.value;
77
+ if (declaration.isImportSpecifier()) {
78
+ const imported = declaration.node.imported;
79
+ return { module, imported: BabelTypes.isIdentifier(imported) ? imported.name : imported.value };
80
+ }
81
+ if (declaration.isImportDefaultSpecifier()) return { module, imported: "default" };
82
+ if (declaration.isImportNamespaceSpecifier()) return { module, imported: "*" };
83
+ }
84
+ if (declaration.isVariableDeclarator() && declaration.get("init").node)
85
+ return importIdentity(declaration.get("init"), depth + 1);
86
+ }
87
+ if (path11.isJSXMemberExpression() || path11.isMemberExpression() && !path11.node.computed) {
88
+ const origin = importIdentity(path11.get("object"), depth + 1);
89
+ const key = path11.node.property;
90
+ if (origin && (BabelTypes.isIdentifier(key) || BabelTypes.isJSXIdentifier(key)) && (origin.imported === "*" || origin.module === "react" && origin.imported === "default"))
91
+ return { module: origin.module, imported: key.name };
92
+ }
93
+ return void 0;
94
+ }
95
+ function handlerFunction(path11, depth = 0) {
96
+ if (depth > MAX_HOPS) return void 0;
97
+ path11 = unwrap(path11);
98
+ if (path11.isFunction()) return path11;
99
+ if (path11.isIdentifier()) {
100
+ const binding = path11.scope.getBinding(path11.node.name);
101
+ if (!binding?.constant) return void 0;
102
+ if (binding.path.isFunctionDeclaration()) return binding.path;
103
+ if (binding.path.isVariableDeclarator() && binding.path.get("init").node)
104
+ return handlerFunction(binding.path.get("init"), depth + 1);
105
+ }
106
+ if (path11.isCallExpression()) {
107
+ const origin = importIdentity(path11.get("callee"));
108
+ if (origin?.module === "react" && origin.imported === "useCallback") {
109
+ const callback = path11.get("arguments")[0];
110
+ if (callback) return handlerFunction(callback, depth + 1);
111
+ }
112
+ }
113
+ if (path11.isMemberExpression() && !path11.node.computed && BabelTypes.isThisExpression(path11.node.object)) {
114
+ const key = path11.node.property;
115
+ if (!BabelTypes.isIdentifier(key)) return void 0;
116
+ const owner = path11.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
117
+ if (!owner || !(owner.isClassDeclaration() || owner.isClassExpression())) return void 0;
118
+ for (const member of owner.get("body").get("body")) {
119
+ if (!(member.isClassMethod() || member.isClassProperty()) || member.node.computed || member.node.static)
120
+ continue;
121
+ if (!BabelTypes.isIdentifier(member.node.key, { name: key.name })) continue;
122
+ if (member.isClassMethod() && member.node.kind === "method") return member;
123
+ if (member.isClassProperty() && member.get("value").node)
124
+ return handlerFunction(member.get("value"), depth + 1);
125
+ }
126
+ }
127
+ return void 0;
128
+ }
129
+
130
+ // src/extractors/control-evidence.ts
131
+ function symbol(node) {
132
+ if (BabelTypes.isTSAsExpression(node) || BabelTypes.isTSTypeAssertion(node) || BabelTypes.isTSNonNullExpression(node) || BabelTypes.isTSSatisfiesExpression(node))
133
+ return symbol(node.expression);
134
+ if (BabelTypes.isIdentifier(node) || BabelTypes.isJSXIdentifier(node)) return node.name;
135
+ if (BabelTypes.isThisExpression(node)) return "this";
136
+ if (BabelTypes.isMemberExpression(node) && !node.computed || BabelTypes.isJSXMemberExpression(node)) {
137
+ const object = symbol(node.object), property = symbol(node.property);
138
+ return object && property ? `${object}.${property}` : void 0;
139
+ }
140
+ if (BabelTypes.isUnaryExpression(node) && node.operator === "!") {
141
+ const value = symbol(node.argument);
142
+ return value ? `!${value}` : void 0;
143
+ }
144
+ return void 0;
145
+ }
146
+ function attribute(node, name) {
147
+ return node.attributes.find(
148
+ (a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name, { name })
149
+ );
150
+ }
151
+ var ICON_LIBRARIES = [
152
+ "lucide-react-native",
153
+ "lucide-react",
154
+ "@tamagui/lucide-icons",
155
+ "@expo/vector-icons",
156
+ "@react-native-vector-icons/",
157
+ "react-native-vector-icons/"
158
+ ];
159
+ function iconLibrary(module) {
160
+ return ICON_LIBRARIES.some(
161
+ (name) => name.endsWith("/") ? module.startsWith(name) : module === name || module.startsWith(name + "/")
162
+ );
163
+ }
164
+ function add(values, value, max = 12) {
165
+ if (value && value.length <= 240 && values.length < max && !values.includes(value))
166
+ values.push(value);
167
+ }
168
+ function iconName(path11, explicitIcon = false) {
169
+ const origin = importIdentity(path11);
170
+ if (origin && iconLibrary(origin.module) && origin.imported !== "*")
171
+ return origin.imported === "default" ? origin.module : `${origin.module}:${origin.imported}`;
172
+ const name = symbol(path11.node);
173
+ return name && (explicitIcon || /icon/i.test(name)) ? name : void 0;
174
+ }
175
+ function iconsInElement(path11, explicitIcon = false) {
176
+ const name = iconName(path11.get("name"), explicitIcon);
177
+ if (!name) return void 0;
178
+ const glyph = propValue(path11, "name");
179
+ const value = glyph && constantValue(glyph);
180
+ return value?.isStringLiteral() ? `${name}:${value.node.value}` : name;
181
+ }
182
+ function hasInteraction(path11) {
183
+ return ["onPress", "onLongPress", "onClick"].some(
184
+ (name) => attribute(path11.node, name) || propValue(path11, name)
185
+ );
186
+ }
187
+ function collectPresentationIcons(path11, icons) {
188
+ const namedIconProps = [
189
+ "icon",
190
+ "prefix",
191
+ "suffix",
192
+ "left",
193
+ "right",
194
+ "leadingIcon",
195
+ "trailingIcon",
196
+ "startIcon",
197
+ "endIcon",
198
+ "renderIcon"
199
+ ];
200
+ const props = new Set(namedIconProps);
201
+ for (const attr of path11.node.attributes)
202
+ if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && !/^on[A-Z]/.test(attr.name.name))
203
+ props.add(attr.name.name);
204
+ for (const prop of props) {
205
+ const icon = propValue(path11, prop);
206
+ if (icon) {
207
+ const value = constantValue(icon);
208
+ if (value?.isStringLiteral()) {
209
+ if (prop.toLowerCase().includes("icon")) add(icons, value.node.value, 8);
210
+ } else {
211
+ const explicitIcon = prop.toLowerCase() === "icon";
212
+ if (icon.isJSXElement()) {
213
+ if (hasInteraction(icon.get("openingElement"))) continue;
214
+ add(icons, iconsInElement(icon.get("openingElement"), explicitIcon), 8);
215
+ }
216
+ icon.traverse({
217
+ JSXAttribute(attr) {
218
+ if (BabelTypes.isJSXIdentifier(attr.node.name) && /^on[A-Z]/.test(attr.node.name.name))
219
+ attr.skip();
220
+ },
221
+ JSXElement(child) {
222
+ const opening = child.get("openingElement");
223
+ if (hasInteraction(opening)) {
224
+ child.skip();
225
+ return;
226
+ }
227
+ add(icons, iconsInElement(opening, explicitIcon), 8);
228
+ }
229
+ });
230
+ if (!icons.length && (namedIconProps.includes(prop) || explicitIcon))
231
+ add(icons, iconName(icon, explicitIcon), 8);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ function collectIcons(path11) {
237
+ const icons = [];
238
+ collectPresentationIcons(path11, icons);
239
+ const jsx = path11.parentPath;
240
+ if (jsx.isJSXElement())
241
+ jsx.traverse({
242
+ // JSX mentioned inside an event callback is not a rendered child icon.
243
+ JSXAttribute(attr) {
244
+ attr.skip();
245
+ },
246
+ JSXElement(child) {
247
+ const opening = child.get("openingElement");
248
+ if (hasInteraction(opening)) {
249
+ child.skip();
250
+ return;
251
+ }
252
+ add(icons, iconsInElement(opening), 8);
253
+ collectPresentationIcons(opening, icons);
254
+ }
255
+ });
256
+ return icons;
257
+ }
258
+ function conditionsAt(path11) {
259
+ const conditions = [];
260
+ let child = path11;
261
+ for (let parent = child.parentPath; parent && !parent.isFunction(); child = parent, parent = parent.parentPath) {
262
+ let test;
263
+ let negated = false;
264
+ if (parent.isLogicalExpression() && parent.node.right === child.node) {
265
+ if (parent.node.operator === "&&") test = parent.node.left;
266
+ if (parent.node.operator === "||") {
267
+ test = parent.node.left;
268
+ negated = true;
269
+ }
270
+ } else if (parent.isConditionalExpression() && parent.node.test !== child.node) {
271
+ test = parent.node.test;
272
+ negated = parent.node.alternate === child.node;
273
+ } else if (parent.isIfStatement() && parent.node.test !== child.node) {
274
+ test = parent.node.test;
275
+ negated = parent.node.alternate === child.node;
276
+ }
277
+ const name = symbol(test);
278
+ if (name) add(conditions, negated ? name.startsWith("!") ? name.slice(1) : `!${name}` : name);
279
+ }
280
+ return conditions;
281
+ }
282
+ function collectHandlerEvidence(expression, evidence) {
283
+ const seen = /* @__PURE__ */ new Set();
284
+ let budget = 100;
285
+ const visit = (path11, depth) => {
286
+ if (depth > 4 || budget-- <= 0) return;
287
+ const fn = handlerFunction(path11);
288
+ if (!fn || seen.has(fn.node)) return;
289
+ seen.add(fn.node);
290
+ fn.traverse({
291
+ // Ignore uncalled helper definitions; inline callbacks remain source evidence.
292
+ Function(nested) {
293
+ if (!nested.parentPath.isCallExpression() && !nested.parentPath.isObjectProperty())
294
+ nested.skip();
295
+ },
296
+ CallExpression(call) {
297
+ if (budget-- <= 0) {
298
+ call.skip();
299
+ return;
300
+ }
301
+ add(evidence.calls, symbol(call.node.callee));
302
+ for (const arg of call.node.arguments) add(evidence.argumentBindings, symbol(arg));
303
+ visit(call.get("callee"), depth + 1);
304
+ if (!BabelTypes.isMemberExpression(call.node.callee) || call.node.callee.computed || !BabelTypes.isIdentifier(call.node.callee.property, { name: "alert" }))
305
+ return;
306
+ const callee = call.get("callee");
307
+ if (!callee.isMemberExpression()) return;
308
+ const origin = importIdentity(callee.get("object"));
309
+ if (origin?.module !== "react-native" || origin.imported !== "Alert") return;
310
+ const options = call.node.arguments[2];
311
+ const destructiveOption = BabelTypes.isArrayExpression(options) && options.elements.some(
312
+ (option) => BabelTypes.isObjectExpression(option) && option.properties.some(
313
+ (p) => BabelTypes.isObjectProperty(p) && !p.computed && symbol(p.key) === "style" && BabelTypes.isStringLiteral(p.value, { value: "destructive" })
314
+ )
315
+ );
316
+ if (destructiveOption)
317
+ evidence.nativeConfirmation = {
318
+ title: symbol(call.node.arguments[0]),
319
+ destructiveOption
320
+ };
321
+ }
322
+ });
323
+ };
324
+ visit(unwrap(expression), 0);
325
+ }
326
+ function extractControlEvidence(ast, source, file) {
327
+ const candidates = [];
328
+ const sourceHash = createHash("sha256").update(source).digest("hex");
329
+ traverse5(ast, {
330
+ JSXOpeningElement(path11) {
331
+ const node = path11.node;
332
+ if (attribute(node, "__appilotsControl")) return;
333
+ const value = propValue(path11, "onPress");
334
+ if (!value && !attribute(node, "onPress") || value?.isNullLiteral() || value?.isJSXEmptyExpression())
335
+ return;
336
+ const icons = collectIcons(path11);
337
+ if (!icons.length || node.end == null) return;
338
+ const evidence = {
339
+ version: 1,
340
+ siteId: createHash("sha256").update(`${file}:${sourceHash}:${node.start}`).digest("hex").slice(0, 20),
341
+ component: symbol(node.name) ?? "unknown",
342
+ icons,
343
+ ...value && symbol(value.node) ? { handler: symbol(value.node) } : {},
344
+ calls: [],
345
+ argumentBindings: [],
346
+ conditions: conditionsAt(path11)
347
+ };
348
+ if (value) collectHandlerEvidence(value, evidence);
349
+ candidates.push({ evidence, sourceHash, offset: node.end - (node.selfClosing ? 2 : 1) });
350
+ }
351
+ });
352
+ return candidates;
353
+ }
354
+ function byCodeUnit(a, b) {
355
+ return a < b ? -1 : a > b ? 1 : 0;
356
+ }
357
+ async function globSorted(patterns, options) {
358
+ const files = await fastGlob(patterns, options);
359
+ return files.sort(byCodeUnit);
360
+ }
17
361
  var DEFAULT_PARSER_PLUGINS = [
18
362
  "jsx",
19
363
  "typescript",
@@ -128,7 +472,7 @@ function classifyJsxComponent(name, element) {
128
472
  }
129
473
  function collectFunctions(ast) {
130
474
  const handlers = /* @__PURE__ */ new Map();
131
- traverse4(ast, {
475
+ traverse5(ast, {
132
476
  FunctionDeclaration: (nodePath) => {
133
477
  if (nodePath.node.id?.name) handlers.set(nodePath.node.id.name, nodePath.node);
134
478
  },
@@ -231,7 +575,7 @@ function analyzeFunctionBehavior(name, fn, handlers, seen = /* @__PURE__ */ new
231
575
  }
232
576
  };
233
577
  if (fn.body) {
234
- traverse4(fn.body, {
578
+ traverse5(fn.body, {
235
579
  noScope: true,
236
580
  enter: (nodePath) => inspectNode(nodePath.node)
237
581
  });
@@ -259,7 +603,7 @@ function setterToStateName(setterName) {
259
603
  }
260
604
  function extractNavigationCalls(ast) {
261
605
  const calls = [];
262
- traverse4(ast, {
606
+ traverse5(ast, {
263
607
  noScope: !BabelTypes.isFile(ast),
264
608
  CallExpression: (nodePath) => {
265
609
  const node = nodePath.node;
@@ -398,14 +742,26 @@ var ScreenAnalyzer = class {
398
742
  strictScreens;
399
743
  /** Glob patterns that identify screen files in strict mode */
400
744
  screenPatterns;
745
+ /**
746
+ * Arquivos que uma ROTA monta (`component={…}` resolvido pelo
747
+ * `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
748
+ * convenção de nome, porque um componente que uma rota monta É uma tela por
749
+ * definição — não é heurística, é o que o app declarou.
750
+ *
751
+ * É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
752
+ * em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
753
+ */
754
+ routeTargetFiles;
401
755
  /** §D: Count of screens filtered out in strict mode (available after analyze()) */
402
756
  screensFilteredOut = 0;
757
+ controlEvidenceFiles = {};
403
758
  constructor(config, options) {
404
759
  this.config = config;
760
+ this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
405
761
  this.strictScreens = options?.strictScreens ?? false;
406
762
  this.screenPatterns = options?.screenPatterns ?? [
407
- "**/*Screen.{ts,tsx}",
408
- "**/screens/**/*.{ts,tsx}"
763
+ "**/*Screen.{ts,tsx,js,jsx}",
764
+ "**/screens/**/*.{ts,tsx,js,jsx}"
409
765
  ];
410
766
  }
411
767
  /** Analyze all screens in the project */
@@ -417,7 +773,7 @@ var ScreenAnalyzer = class {
417
773
  console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
418
774
  }
419
775
  }
420
- const files = await glob(include, {
776
+ const files = await globSorted(include, {
421
777
  cwd: this.config.rootDir,
422
778
  ignore: exclude
423
779
  });
@@ -426,23 +782,24 @@ var ScreenAnalyzer = class {
426
782
  }
427
783
  let screenPatternFiles = null;
428
784
  if (this.strictScreens) {
429
- const matched = await glob(this.screenPatterns, {
785
+ const matched = await globSorted(this.screenPatterns, {
430
786
  cwd: this.config.rootDir,
431
787
  ignore: exclude
432
788
  });
433
- screenPatternFiles = new Set(matched.map((f) => path8__default.resolve(this.config.rootDir, f)));
789
+ screenPatternFiles = new Set(matched.map((f) => path2__default.resolve(this.config.rootDir, f)));
434
790
  }
435
791
  const screens = [];
436
792
  this.screensFilteredOut = 0;
437
793
  for (const file of files) {
438
- const filePath = path8__default.resolve(this.config.rootDir, file);
794
+ const filePath = path2__default.resolve(this.config.rootDir, file);
439
795
  try {
440
796
  const descriptor = await this.analyzeFile(filePath);
441
797
  if (!descriptor) continue;
442
798
  if (this.strictScreens) {
443
799
  const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
444
800
  const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
445
- if (!hasRegisterScreen && !matchesPattern) {
801
+ const isRouteTarget = this.routeTargetFiles.has(filePath);
802
+ if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
446
803
  this.screensFilteredOut++;
447
804
  if (this.verbose) {
448
805
  console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
@@ -498,6 +855,11 @@ var ScreenAnalyzer = class {
498
855
  title: registerScreenMeta?.title,
499
856
  description: registerScreenMeta?.description,
500
857
  components,
858
+ controlCandidates: extractControlEvidence(
859
+ ast,
860
+ source,
861
+ path2__default.relative(this.config.rootDir, filePath)
862
+ ),
501
863
  forms,
502
864
  actions,
503
865
  navigationTargets,
@@ -508,6 +870,8 @@ var ScreenAnalyzer = class {
508
870
  ...permissionsFromJsDoc.isPii ? { isPii: true } : {}
509
871
  } : {}
510
872
  };
873
+ if (descriptor.controlCandidates?.length)
874
+ this.controlEvidenceFiles[path2__default.relative(this.config.rootDir, filePath).split(path2__default.sep).join("/")] = descriptor.controlCandidates;
511
875
  descriptor.__hasRegisterScreen = hasRegisterScreenCall;
512
876
  return descriptor;
513
877
  }
@@ -517,7 +881,7 @@ var ScreenAnalyzer = class {
517
881
  */
518
882
  detectRegisterScreenCall(ast) {
519
883
  let found = false;
520
- traverse4(ast, {
884
+ traverse5(ast, {
521
885
  CallExpression: (nodePath) => {
522
886
  if (found) return;
523
887
  const callee = nodePath.node.callee;
@@ -534,7 +898,7 @@ var ScreenAnalyzer = class {
534
898
  */
535
899
  extractRegisterScreenMetadata(ast) {
536
900
  let metadata = null;
537
- traverse4(ast, {
901
+ traverse5(ast, {
538
902
  CallExpression: (nodePath) => {
539
903
  const callee = nodePath.node.callee;
540
904
  if (BabelTypes.isIdentifier(callee) && callee.name === "registerScreen" || BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.property) && callee.property.name === "registerScreen") {
@@ -650,6 +1014,8 @@ var ScreenAnalyzer = class {
650
1014
  action.riskLevel = value.value;
651
1015
  } else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
652
1016
  action.nativeConfirmationExpected = value.value;
1017
+ } else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
1018
+ action.asyncBudgetMs = value.value;
653
1019
  } else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
654
1020
  action.appilotsInferred = this.parseAppilotsInferredObject(value);
655
1021
  }
@@ -829,7 +1195,7 @@ var ScreenAnalyzer = class {
829
1195
  */
830
1196
  extractDefaultComponentName(ast) {
831
1197
  let componentName = "";
832
- traverse4(ast, {
1198
+ traverse5(ast, {
833
1199
  ExportDefaultDeclaration: (nodePath) => {
834
1200
  const declaration = nodePath.node.declaration;
835
1201
  if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
@@ -854,7 +1220,7 @@ var ScreenAnalyzer = class {
854
1220
  */
855
1221
  extractNavigationTargets(ast) {
856
1222
  const targets = /* @__PURE__ */ new Set();
857
- traverse4(ast, {
1223
+ traverse5(ast, {
858
1224
  CallExpression: (nodePath) => {
859
1225
  const callee = nodePath.node.callee;
860
1226
  if (BabelTypes.isMemberExpression(callee) && BabelTypes.isIdentifier(callee.object) && callee.object.name === "navigation" && BabelTypes.isIdentifier(callee.property) && callee.property.name === "navigate") {
@@ -873,7 +1239,7 @@ var ScreenAnalyzer = class {
873
1239
  extractForms(ast) {
874
1240
  const forms = [];
875
1241
  const fields = /* @__PURE__ */ new Map();
876
- traverse4(ast, {
1242
+ traverse5(ast, {
877
1243
  JSXOpeningElement: (nodePath) => {
878
1244
  const element = nodePath.node;
879
1245
  if (BabelTypes.isJSXIdentifier(element.name)) {
@@ -994,7 +1360,7 @@ var ScreenAnalyzer = class {
994
1360
  extractComponents(ast) {
995
1361
  const components = [];
996
1362
  const seen = /* @__PURE__ */ new Set();
997
- traverse4(ast, {
1363
+ traverse5(ast, {
998
1364
  JSXOpeningElement: (nodePath) => {
999
1365
  const element = nodePath.node;
1000
1366
  if (BabelTypes.isJSXIdentifier(element.name)) {
@@ -1058,7 +1424,7 @@ var ScreenAnalyzer = class {
1058
1424
  const actionLabels = new Map(
1059
1425
  actions.filter((a) => a.label).map((a) => [this.normalizeLabel(a.label), a])
1060
1426
  );
1061
- traverse4(ast, {
1427
+ traverse5(ast, {
1062
1428
  JSXOpeningElement: (nodePath) => {
1063
1429
  const element = nodePath.node;
1064
1430
  if (BabelTypes.isJSXIdentifier(element.name)) {
@@ -1127,7 +1493,7 @@ var ScreenAnalyzer = class {
1127
1493
  }
1128
1494
  collectButtonHandlersByLabel(ast) {
1129
1495
  const out = /* @__PURE__ */ new Map();
1130
- traverse4(ast, {
1496
+ traverse5(ast, {
1131
1497
  JSXOpeningElement: (nodePath) => {
1132
1498
  const element = nodePath.node;
1133
1499
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
@@ -1209,7 +1575,7 @@ var ScreenAnalyzer = class {
1209
1575
  }
1210
1576
  };
1211
1577
  if (fn.body) {
1212
- traverse4(
1578
+ traverse5(
1213
1579
  fn.body,
1214
1580
  {
1215
1581
  noScope: true,
@@ -1406,7 +1772,7 @@ var ScreenAnalyzer = class {
1406
1772
  extractCollections(ast) {
1407
1773
  const renderItemFns = this.collectRenderItemFunctions(ast);
1408
1774
  const collections = [];
1409
- traverse4(ast, {
1775
+ traverse5(ast, {
1410
1776
  JSXOpeningElement: (nodePath) => {
1411
1777
  const element = nodePath.node;
1412
1778
  if (!BabelTypes.isJSXIdentifier(element.name)) return;
@@ -1443,7 +1809,7 @@ var ScreenAnalyzer = class {
1443
1809
  }
1444
1810
  collectRenderItemFunctions(ast) {
1445
1811
  const out = /* @__PURE__ */ new Map();
1446
- traverse4(ast, {
1812
+ traverse5(ast, {
1447
1813
  VariableDeclarator: (nodePath) => {
1448
1814
  if (!BabelTypes.isIdentifier(nodePath.node.id)) return;
1449
1815
  const init = nodePath.node.init;
@@ -1486,7 +1852,7 @@ var ScreenAnalyzer = class {
1486
1852
  }
1487
1853
  extractRowAction(fn) {
1488
1854
  let action;
1489
- traverse4(
1855
+ traverse5(
1490
1856
  fn.body,
1491
1857
  {
1492
1858
  noScope: true,
@@ -1539,7 +1905,7 @@ var ScreenAnalyzer = class {
1539
1905
  } else if (BabelTypes.isIdentifier(firstParam)) {
1540
1906
  itemNames.add(firstParam.name);
1541
1907
  }
1542
- traverse4(
1908
+ traverse5(
1543
1909
  fn.body,
1544
1910
  {
1545
1911
  noScope: true,
@@ -1581,7 +1947,7 @@ var ScreenAnalyzer = class {
1581
1947
  inferSearchField(ast, dataSource) {
1582
1948
  if (!dataSource) return void 0;
1583
1949
  let queryBinding;
1584
- traverse4(ast, {
1950
+ traverse5(ast, {
1585
1951
  CallExpression: (nodePath) => {
1586
1952
  const node = nodePath.node;
1587
1953
  if (!BabelTypes.isMemberExpression(node.callee)) return;
@@ -1592,7 +1958,7 @@ var ScreenAnalyzer = class {
1592
1958
  const fn = node.arguments[0];
1593
1959
  if (!BabelTypes.isArrowFunctionExpression(fn) && !BabelTypes.isFunctionExpression(fn))
1594
1960
  return;
1595
- traverse4(
1961
+ traverse5(
1596
1962
  fn.body,
1597
1963
  {
1598
1964
  noScope: true,
@@ -1629,43 +1995,871 @@ var ScreenAnalyzer = class {
1629
1995
  * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1630
1996
  */
1631
1997
  extractScreenName(filePath) {
1632
- const basename2 = path8__default.basename(filePath);
1998
+ const basename2 = path2__default.basename(filePath);
1633
1999
  return basename2.replace(/\.(tsx?|jsx?)$/, "");
1634
2000
  }
1635
2001
  };
2002
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
2003
+ var MAX_HOPS2 = 8;
2004
+ var ModuleGraph = class {
2005
+ asts = /* @__PURE__ */ new Map();
2006
+ resolved = /* @__PURE__ */ new Map();
2007
+ /** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
2008
+ aliases;
2009
+ /** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
2010
+ workspacePackages;
2011
+ constructor(rootDir) {
2012
+ this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
2013
+ this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
2014
+ }
2015
+ /** AST de um arquivo, memoizada. `null` quando não parseia. */
2016
+ parse(file) {
2017
+ const cached = this.asts.get(file);
2018
+ if (cached !== void 0) return cached;
2019
+ let ast = null;
2020
+ try {
2021
+ ast = parser.parse(readFileSync(file, "utf-8"), {
2022
+ sourceType: "module",
2023
+ plugins: ["jsx", "typescript"]
2024
+ });
2025
+ } catch {
2026
+ ast = null;
2027
+ }
2028
+ this.asts.set(file, ast);
2029
+ return ast;
2030
+ }
2031
+ /**
2032
+ * `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
2033
+ * Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
2034
+ * é de terceiro e não tem tela nossa dentro.
2035
+ */
2036
+ resolve(fromFile, spec) {
2037
+ const key = `${fromFile} ${spec}`;
2038
+ const cached = this.resolved.get(key);
2039
+ if (cached !== void 0) return cached;
2040
+ let base = null;
2041
+ if (spec.startsWith(".")) {
2042
+ base = path2__default.resolve(path2__default.dirname(fromFile), spec);
2043
+ } else {
2044
+ for (const { prefix, target } of this.aliases) {
2045
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
2046
+ base = path2__default.join(target, spec.slice(prefix.length));
2047
+ break;
2048
+ }
2049
+ }
2050
+ if (!base) {
2051
+ for (const pkg of this.workspacePackages) {
2052
+ if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
2053
+ base = path2__default.join(pkg.dir, spec.slice(pkg.name.length));
2054
+ break;
2055
+ }
2056
+ }
2057
+ }
2058
+ }
2059
+ if (!base) {
2060
+ this.resolved.set(key, null);
2061
+ return null;
2062
+ }
2063
+ const candidates = [
2064
+ base,
2065
+ ...EXTENSIONS.map((e) => base + e),
2066
+ ...EXTENSIONS.map((e) => path2__default.join(base, "index" + e))
2067
+ ];
2068
+ let found = null;
2069
+ for (const c of candidates) {
2070
+ try {
2071
+ if (existsSync(c) && statSync(c).isFile()) {
2072
+ found = c;
2073
+ break;
2074
+ }
2075
+ } catch {
2076
+ }
2077
+ }
2078
+ this.resolved.set(key, found);
2079
+ return found;
2080
+ }
2081
+ /** `import`s do arquivo, por nome local. */
2082
+ imports(file) {
2083
+ const out = /* @__PURE__ */ new Map();
2084
+ const ast = this.parse(file);
2085
+ if (!ast) return out;
2086
+ for (const stmt of ast.program.body) {
2087
+ if (!BabelTypes.isImportDeclaration(stmt)) continue;
2088
+ const source = stmt.source.value;
2089
+ for (const spec of stmt.specifiers) {
2090
+ if (BabelTypes.isImportDefaultSpecifier(spec)) {
2091
+ out.set(spec.local.name, { source, imported: "default" });
2092
+ } else if (BabelTypes.isImportNamespaceSpecifier(spec)) {
2093
+ out.set(spec.local.name, { source, imported: "*" });
2094
+ } else if (BabelTypes.isImportSpecifier(spec)) {
2095
+ const imported = BabelTypes.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
2096
+ out.set(spec.local.name, { source, imported });
2097
+ }
2098
+ }
2099
+ }
2100
+ return out;
2101
+ }
2102
+ /** `const X = <init>` no topo do arquivo, incluindo `export const`. */
2103
+ topLevelInit(file, name) {
2104
+ const ast = this.parse(file);
2105
+ if (!ast) return null;
2106
+ for (const stmt of ast.program.body) {
2107
+ const decl = BabelTypes.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
2108
+ if (!BabelTypes.isVariableDeclaration(decl)) continue;
2109
+ for (const d of decl.declarations) {
2110
+ if (BabelTypes.isIdentifier(d.id) && d.id.name === name && d.init) {
2111
+ return BabelTypes.isTSAsExpression(d.init) ? d.init.expression : d.init;
2112
+ }
2113
+ }
2114
+ }
2115
+ return null;
2116
+ }
2117
+ /**
2118
+ * Onde `name` é DEFINIDO — segue import e reexport de barrel.
2119
+ * Devolve o arquivo e o nome sob o qual ele é definido lá.
2120
+ */
2121
+ resolveBinding(file, name, hops = 0) {
2122
+ if (hops > MAX_HOPS2) return null;
2123
+ if (this.topLevelInit(file, name) !== null) return { file, name };
2124
+ const binding = this.imports(file).get(name);
2125
+ if (binding) {
2126
+ const target = this.resolve(file, binding.source);
2127
+ if (!target) return null;
2128
+ const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
2129
+ const deeper = this.resolveBinding(target, next, hops + 1);
2130
+ return deeper ?? { file: target, name: next };
2131
+ }
2132
+ const ast = this.parse(file);
2133
+ if (ast) {
2134
+ for (const stmt of ast.program.body) {
2135
+ if (!BabelTypes.isExportNamedDeclaration(stmt) || !stmt.source) continue;
2136
+ for (const spec of stmt.specifiers) {
2137
+ if (!BabelTypes.isExportSpecifier(spec)) continue;
2138
+ const exported = BabelTypes.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
2139
+ if (exported !== name) continue;
2140
+ const target = this.resolve(file, stmt.source.value);
2141
+ if (!target) return null;
2142
+ const local = spec.local.name;
2143
+ return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
2144
+ }
2145
+ }
2146
+ }
2147
+ if (ast) {
2148
+ for (const stmt of ast.program.body) {
2149
+ if (!BabelTypes.isExportAllDeclaration(stmt)) continue;
2150
+ const target = this.resolve(file, stmt.source.value);
2151
+ if (!target || target === file) continue;
2152
+ const deeper = this.resolveBinding(target, name, hops + 1);
2153
+ if (deeper) return deeper;
2154
+ }
2155
+ }
2156
+ if (ast) {
2157
+ for (const stmt of ast.program.body) {
2158
+ if (!BabelTypes.isExportDefaultDeclaration(stmt)) continue;
2159
+ if (BabelTypes.isIdentifier(stmt.declaration)) {
2160
+ const local = stmt.declaration.name;
2161
+ if (local === name) return null;
2162
+ return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
2163
+ }
2164
+ }
2165
+ }
2166
+ return null;
2167
+ }
2168
+ /**
2169
+ * O valor string de uma expressão de nome de rota, ou `null`.
2170
+ *
2171
+ * Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
2172
+ * seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
2173
+ * com interpolação nem valor calculado, de propósito.
2174
+ */
2175
+ stringConstant(file, node, hops = 0) {
2176
+ if (!node || hops > MAX_HOPS2) return null;
2177
+ if (BabelTypes.isStringLiteral(node)) return node.value;
2178
+ if (BabelTypes.isTemplateLiteral(node)) {
2179
+ return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
2180
+ }
2181
+ if (BabelTypes.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
2182
+ const chain = memberChain(node);
2183
+ if (!chain) return null;
2184
+ const origin = this.resolveBinding(file, chain.root);
2185
+ if (!origin) return null;
2186
+ let current = this.topLevelInit(origin.file, origin.name);
2187
+ if (!current) return null;
2188
+ for (const key of chain.path) {
2189
+ if (!BabelTypes.isObjectExpression(current)) return null;
2190
+ const prop = objectProperty(current, key);
2191
+ if (!prop) return null;
2192
+ current = BabelTypes.isTSAsExpression(prop) ? prop.expression : prop;
2193
+ }
2194
+ return BabelTypes.isStringLiteral(current) ? current.value : null;
2195
+ }
2196
+ /**
2197
+ * O ARQUIVO onde vive o componente de uma rota, ou `null`.
2198
+ *
2199
+ * Aceita as três formas que o corpus mostrou: identificador
2200
+ * (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
2201
+ * e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
2202
+ * que é como o `pocketpal` monta todas as telas do Drawer).
2203
+ */
2204
+ componentFile(file, node, hops = 0) {
2205
+ if (!node || hops > MAX_HOPS2) return null;
2206
+ if (BabelTypes.isCallExpression(node)) {
2207
+ for (const arg of node.arguments) {
2208
+ if (BabelTypes.isIdentifier(arg) || BabelTypes.isMemberExpression(arg)) {
2209
+ const inner = this.componentFile(file, arg, hops + 1);
2210
+ if (inner) return inner;
2211
+ }
2212
+ }
2213
+ return null;
2214
+ }
2215
+ if (BabelTypes.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
2216
+ const chain = memberChain(node);
2217
+ if (!chain) return null;
2218
+ const origin = this.resolveBinding(file, chain.root);
2219
+ if (!origin) return null;
2220
+ if (chain.path.length === 0) return origin.file;
2221
+ const init = this.topLevelInit(origin.file, origin.name);
2222
+ if (init && BabelTypes.isObjectExpression(init)) {
2223
+ const prop = objectProperty(init, chain.path[0]);
2224
+ if (prop) return this.componentFile(origin.file, prop, hops + 1);
2225
+ }
2226
+ const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
2227
+ return viaExport?.file ?? null;
2228
+ }
2229
+ /**
2230
+ * Os arquivos DO PROJETO que este arquivo renderiza como JSX.
2231
+ *
2232
+ * `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
2233
+ * Import de pacote devolve `null` no `resolve` e fica de fora: componente de
2234
+ * terceiro não tem tela nossa dentro.
2235
+ *
2236
+ * Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
2237
+ * extractor, e um navegador não é conteúdo de tela.
2238
+ */
2239
+ renderedComponentFiles(file) {
2240
+ const ast = this.parse(file);
2241
+ if (!ast) return [];
2242
+ const names = /* @__PURE__ */ new Set();
2243
+ const visit = (node) => {
2244
+ if (!node || typeof node !== "object") return;
2245
+ if (BabelTypes.isJSXOpeningElement(node) && BabelTypes.isJSXIdentifier(node.name)) {
2246
+ const n = node.name.name;
2247
+ if (/^[A-Z]/.test(n)) names.add(n);
2248
+ }
2249
+ for (const key of Object.keys(node)) {
2250
+ const value = node[key];
2251
+ if (Array.isArray(value)) {
2252
+ for (const item of value) visit(item);
2253
+ } else if (value && typeof value === "object" && "type" in value) {
2254
+ visit(value);
2255
+ }
2256
+ }
2257
+ };
2258
+ visit(ast.program);
2259
+ const out = /* @__PURE__ */ new Set();
2260
+ for (const name of names) {
2261
+ const origin = this.resolveBinding(file, name);
2262
+ if (origin && origin.file !== file) out.add(origin.file);
2263
+ }
2264
+ return [...out];
2265
+ }
2266
+ };
2267
+ function memberChain(node) {
2268
+ const chain = [];
2269
+ let current = node;
2270
+ while (BabelTypes.isMemberExpression(current)) {
2271
+ if (current.computed) {
2272
+ if (!BabelTypes.isStringLiteral(current.property)) return null;
2273
+ chain.unshift(current.property.value);
2274
+ } else if (BabelTypes.isIdentifier(current.property)) {
2275
+ chain.unshift(current.property.name);
2276
+ } else {
2277
+ return null;
2278
+ }
2279
+ current = current.object;
2280
+ }
2281
+ return BabelTypes.isIdentifier(current) ? { root: current.name, path: chain } : null;
2282
+ }
2283
+ function objectProperty(obj, key) {
2284
+ for (const prop of obj.properties) {
2285
+ if (!BabelTypes.isObjectProperty(prop)) continue;
2286
+ const name = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
2287
+ if (name === key && BabelTypes.isExpression(prop.value)) return prop.value;
2288
+ }
2289
+ return null;
2290
+ }
2291
+ function readTsconfigAliases(rootDir) {
2292
+ const file = path2__default.join(rootDir, "tsconfig.json");
2293
+ if (!existsSync(file)) return [];
2294
+ let parsed;
2295
+ try {
2296
+ const raw = stripJsonComments(readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
2297
+ parsed = JSON.parse(raw);
2298
+ } catch {
2299
+ return [];
2300
+ }
2301
+ const paths = parsed.compilerOptions?.paths;
2302
+ if (!paths) return [];
2303
+ const baseUrl = path2__default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
2304
+ const out = [];
2305
+ for (const [pattern, targets] of Object.entries(paths)) {
2306
+ const first = targets[0];
2307
+ if (typeof first !== "string") continue;
2308
+ out.push({
2309
+ prefix: pattern.replace(/\/?\*$/, ""),
2310
+ target: path2__default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
2311
+ });
2312
+ }
2313
+ return out.sort((a, b) => b.prefix.length - a.prefix.length);
2314
+ }
2315
+ function stripJsonComments(text) {
2316
+ let out = "";
2317
+ let inString = false;
2318
+ let escaped = false;
2319
+ for (let i = 0; i < text.length; i++) {
2320
+ const c = text[i];
2321
+ if (inString) {
2322
+ out += c;
2323
+ if (escaped) escaped = false;
2324
+ else if (c === "\\") escaped = true;
2325
+ else if (c === '"') inString = false;
2326
+ continue;
2327
+ }
2328
+ if (c === '"') {
2329
+ inString = true;
2330
+ out += c;
2331
+ continue;
2332
+ }
2333
+ if (c === "/" && text[i + 1] === "/") {
2334
+ while (i < text.length && text[i] !== "\n") i++;
2335
+ out += "\n";
2336
+ continue;
2337
+ }
2338
+ if (c === "/" && text[i + 1] === "*") {
2339
+ i += 2;
2340
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
2341
+ i++;
2342
+ continue;
2343
+ }
2344
+ out += c;
2345
+ }
2346
+ return out;
2347
+ }
2348
+ function readWorkspacePackages(rootDir) {
2349
+ const start = path2__default.resolve(rootDir);
2350
+ let dir = start;
2351
+ for (let i = 0; i < 6; i++) {
2352
+ const globs = workspaceGlobs(dir);
2353
+ if (globs.length > 0) {
2354
+ const packages = expandWorkspaceGlobs(dir, globs);
2355
+ const containsApp = packages.some(
2356
+ (p) => start === p.dir || start.startsWith(p.dir + path2__default.sep)
2357
+ );
2358
+ return containsApp ? packages : [];
2359
+ }
2360
+ const parent = path2__default.dirname(dir);
2361
+ if (parent === dir) break;
2362
+ dir = parent;
2363
+ }
2364
+ return [];
2365
+ }
2366
+ function workspaceGlobs(dir) {
2367
+ const pnpm = path2__default.join(dir, "pnpm-workspace.yaml");
2368
+ if (existsSync(pnpm)) {
2369
+ try {
2370
+ const lines = readFileSync(pnpm, "utf-8").split(/\r?\n/);
2371
+ const out = [];
2372
+ let inPackages = false;
2373
+ for (const line of lines) {
2374
+ if (/^packages:/.test(line)) {
2375
+ inPackages = true;
2376
+ continue;
2377
+ }
2378
+ if (inPackages) {
2379
+ const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
2380
+ if (m) out.push(m[1].trim());
2381
+ else if (/^\S/.test(line)) break;
2382
+ }
2383
+ }
2384
+ if (out.length) return out;
2385
+ } catch {
2386
+ }
2387
+ }
2388
+ const pkgPath = path2__default.join(dir, "package.json");
2389
+ if (!existsSync(pkgPath)) return [];
2390
+ try {
2391
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2392
+ const ws = pkg.workspaces;
2393
+ if (Array.isArray(ws)) return ws;
2394
+ if (ws && Array.isArray(ws.packages)) return ws.packages;
2395
+ } catch {
2396
+ }
2397
+ return [];
2398
+ }
2399
+ function expandWorkspaceGlobs(root, globs) {
2400
+ const out = [];
2401
+ const add2 = (dir) => {
2402
+ const pkgPath = path2__default.join(dir, "package.json");
2403
+ if (!existsSync(pkgPath)) return;
2404
+ try {
2405
+ const name = JSON.parse(readFileSync(pkgPath, "utf-8")).name;
2406
+ if (name) out.push({ name, dir });
2407
+ } catch {
2408
+ }
2409
+ };
2410
+ for (const glob of globs) {
2411
+ if (glob.endsWith("/*")) {
2412
+ const parent = path2__default.join(root, glob.slice(0, -2));
2413
+ let entries = [];
2414
+ try {
2415
+ entries = readdirSync(parent);
2416
+ } catch {
2417
+ continue;
2418
+ }
2419
+ for (const entry of entries) {
2420
+ const dir = path2__default.join(parent, entry);
2421
+ try {
2422
+ if (statSync(dir).isDirectory()) add2(dir);
2423
+ } catch {
2424
+ }
2425
+ }
2426
+ } else if (!glob.includes("*")) {
2427
+ add2(path2__default.join(root, glob));
2428
+ }
2429
+ }
2430
+ return out.sort((a, b) => b.name.length - a.name.length);
2431
+ }
2432
+ var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
2433
+ var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
2434
+ var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
2435
+ function baseName(file) {
2436
+ let name = file;
2437
+ for (const ext of ROUTE_EXTENSIONS) {
2438
+ if (name.endsWith(ext)) {
2439
+ name = name.slice(0, -ext.length);
2440
+ break;
2441
+ }
2442
+ }
2443
+ for (const suffix of PLATFORM_SUFFIXES) {
2444
+ if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
2445
+ }
2446
+ return name;
2447
+ }
2448
+ function isRouteFile(file) {
2449
+ return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
2450
+ }
2451
+ function isSpecial(base) {
2452
+ if (base === "_layout") return true;
2453
+ if (base.startsWith("+")) return base !== "+not-found";
2454
+ return base.startsWith("_");
2455
+ }
2456
+ function layoutType(file) {
2457
+ try {
2458
+ const src = readFileSync(file, "utf-8");
2459
+ if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
2460
+ if (/<Drawer\b/.test(src)) return "drawer";
2461
+ } catch {
2462
+ }
2463
+ return "stack";
2464
+ }
2465
+ function findLayout(dir) {
2466
+ for (const ext of ROUTE_EXTENSIONS) {
2467
+ const candidate = path2__default.join(dir, "_layout" + ext);
2468
+ if (existsSync(candidate)) return candidate;
2469
+ }
2470
+ return null;
2471
+ }
2472
+ function analyzeExpoRouter(appRoot) {
2473
+ let routeDir = null;
2474
+ for (const candidate of ROUTE_DIR_CANDIDATES) {
2475
+ const full = path2__default.join(appRoot, candidate);
2476
+ if (existsSync(full) && findLayout(full)) {
2477
+ routeDir = full;
2478
+ break;
2479
+ }
2480
+ }
2481
+ if (!routeDir) return null;
2482
+ const routes = [];
2483
+ const navigators = /* @__PURE__ */ new Map();
2484
+ const seen = /* @__PURE__ */ new Set();
2485
+ const walk = (dir, segments, navigator) => {
2486
+ const layout = findLayout(dir);
2487
+ const current = layout ? segments.join("/") || "/" : navigator;
2488
+ if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
2489
+ let entries;
2490
+ try {
2491
+ entries = readdirSync(dir);
2492
+ } catch {
2493
+ return;
2494
+ }
2495
+ for (const entry of entries.sort()) {
2496
+ const full = path2__default.join(dir, entry);
2497
+ let isDir = false;
2498
+ try {
2499
+ isDir = statSync(full).isDirectory();
2500
+ } catch {
2501
+ continue;
2502
+ }
2503
+ if (isDir) {
2504
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
2505
+ walk(full, [...segments, entry], current);
2506
+ continue;
2507
+ }
2508
+ if (!isRouteFile(entry)) continue;
2509
+ const base = baseName(entry);
2510
+ if (isSpecial(base)) continue;
2511
+ const routeSegments = base === "index" ? segments : [...segments, base];
2512
+ const name = "/" + routeSegments.join("/");
2513
+ if (seen.has(name)) continue;
2514
+ seen.add(name);
2515
+ routes.push({
2516
+ name,
2517
+ componentFile: full,
2518
+ navigatorName: current,
2519
+ navigatorType: navigators.get(current) ?? "stack",
2520
+ params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
2521
+ });
2522
+ }
2523
+ };
2524
+ walk(routeDir, [], "/");
2525
+ return {
2526
+ routeDir: path2__default.relative(appRoot, routeDir),
2527
+ routes,
2528
+ navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
2529
+ };
2530
+ }
2531
+
2532
+ // src/analyzers/NavigationAnalyzer.ts
2533
+ var NAVIGATOR_FACTORIES = {
2534
+ createStackNavigator: "stack",
2535
+ createNativeStackNavigator: "stack",
2536
+ createBottomTabNavigator: "tab",
2537
+ createTabNavigator: "tab",
2538
+ createMaterialTopTabNavigator: "tab",
2539
+ createMaterialBottomTabNavigator: "tab",
2540
+ createDrawerNavigator: "drawer"
2541
+ };
2542
+ var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
2543
+ function unwrapStaticScreen(value) {
2544
+ if (BabelTypes.isCallExpression(value)) {
2545
+ const arg = value.arguments[0];
2546
+ return arg ? unwrapStaticScreen(arg) : null;
2547
+ }
2548
+ if (BabelTypes.isObjectExpression(value)) {
2549
+ for (const prop of value.properties) {
2550
+ if (!BabelTypes.isObjectProperty(prop)) continue;
2551
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
2552
+ if (key === "screen" && BabelTypes.isExpression(prop.value)) return prop.value;
2553
+ }
2554
+ return null;
2555
+ }
2556
+ return BabelTypes.isExpression(value) ? value : null;
2557
+ }
2558
+ function inferNavigatorType(factoryName) {
2559
+ if (/drawer/i.test(factoryName)) return "drawer";
2560
+ if (/tab/i.test(factoryName)) return "tab";
2561
+ return "stack";
2562
+ }
2563
+ var NAVIGATION_EVIDENCE = new RegExp(
2564
+ // `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
2565
+ // não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
2566
+ // `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
2567
+ // que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
2568
+ // junção entre arquivos nunca chega a ser tentada.
2569
+ // Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
2570
+ // fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
2571
+ // seis navegadores do `bluesky`.
2572
+ `create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
2573
+ );
1636
2574
  var NavigationAnalyzer = class {
1637
2575
  config;
1638
2576
  /** Extra glob patterns for navigation file discovery (added to defaults) */
1639
2577
  navigationInclude;
1640
2578
  /** Extra glob patterns to exclude from navigation analysis */
1641
2579
  navigationExclude;
2580
+ /** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
2581
+ graph;
2582
+ /**
2583
+ * Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
2584
+ * de navegação e a árvore de telas; sem ela as duas metades do documento
2585
+ * falam de coisas diferentes. Populada por `analyze()`.
2586
+ */
2587
+ routeTargets = /* @__PURE__ */ new Map();
2588
+ /**
2589
+ * Arquivos que DECLARAM uma fábrica de navegador.
2590
+ *
2591
+ * Um navegador aninhado é montado como `component=` de uma rota — no
2592
+ * `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
2593
+ * Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
2594
+ * exatamente o falso positivo que o corpus existe para não repetir. Um
2595
+ * navegador é um contêiner de rotas; a tela está um nível abaixo.
2596
+ *
2597
+ * Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
2598
+ * RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
2599
+ * `DetailViewStack` num arquivo e escreve o JSX em
2600
+ * `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
2601
+ * segundo entrar como tela.
2602
+ */
2603
+ navigatorFiles = /* @__PURE__ */ new Set();
2604
+ /** Preenchido por `analyze()`. Vazio antes disso. */
2605
+ diagnostics = {
2606
+ filesScanned: 0,
2607
+ filesWithEvidence: 0,
2608
+ navigatorsDeclared: 0,
2609
+ navigatorsWithJsx: 0,
2610
+ routes: 0,
2611
+ routesLinkedToFile: 0
2612
+ };
1642
2613
  constructor(config, options) {
1643
2614
  this.config = config;
2615
+ this.graph = new ModuleGraph(config.rootDir);
1644
2616
  this.navigationInclude = options?.navigationInclude ?? [];
1645
2617
  this.navigationExclude = options?.navigationExclude ?? [];
1646
2618
  }
1647
2619
  /** Build the full navigation graph */
1648
2620
  async analyze() {
1649
2621
  const navigationFiles = await this.findNavigationFiles();
1650
- const parsedNavigators = [];
1651
2622
  const typeExports = [];
2623
+ this.routeTargets.clear();
2624
+ const declarations = /* @__PURE__ */ new Map();
2625
+ const evidenceFiles = [];
2626
+ this.navigatorFiles.clear();
1652
2627
  for (const filePath of navigationFiles) {
1653
2628
  try {
1654
2629
  const content = await promises.readFile(filePath, "utf-8");
1655
- const navigators = this.parseNavigators(content, filePath);
1656
- const types = this.parseParamTypes(content, filePath);
1657
- parsedNavigators.push(...navigators);
1658
- typeExports.push(...types);
2630
+ if (!NAVIGATION_EVIDENCE.test(content)) continue;
2631
+ evidenceFiles.push(filePath);
2632
+ for (const decl of this.collectDeclarations(filePath)) {
2633
+ declarations.set(`${decl.file}#${decl.name}`, decl);
2634
+ if (decl.confident) this.navigatorFiles.add(decl.file);
2635
+ }
2636
+ typeExports.push(...this.parseParamTypes(content, filePath));
1659
2637
  } catch (error) {
1660
2638
  console.warn(`Failed to parse ${filePath}:`, error);
1661
2639
  }
1662
2640
  }
2641
+ const parsedNavigators = [];
2642
+ for (const filePath of evidenceFiles) {
2643
+ try {
2644
+ parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
2645
+ } catch (error) {
2646
+ console.warn(`Failed to parse navigators in ${filePath}:`, error);
2647
+ }
2648
+ }
2649
+ let detachedScreens = 0;
2650
+ for (const filePath of evidenceFiles) {
2651
+ try {
2652
+ for (const detached of this.parseDetachedScreens(filePath, declarations)) {
2653
+ const existing = parsedNavigators.find(
2654
+ (nav) => nav.name === detached.name && nav.type === detached.type
2655
+ );
2656
+ if (!existing) {
2657
+ parsedNavigators.push(detached);
2658
+ detachedScreens += detached.screens.length;
2659
+ continue;
2660
+ }
2661
+ const seen = new Set(existing.screens.map((screen) => screen.name));
2662
+ for (const screen of detached.screens) {
2663
+ if (seen.has(screen.name)) continue;
2664
+ seen.add(screen.name);
2665
+ existing.screens.push(screen);
2666
+ detachedScreens++;
2667
+ }
2668
+ }
2669
+ } catch (error) {
2670
+ console.warn(`Failed to parse detached screens in ${filePath}:`, error);
2671
+ }
2672
+ }
2673
+ const expo = analyzeExpoRouter(this.config.rootDir);
2674
+ if (expo) {
2675
+ console.log(
2676
+ `[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
2677
+ );
2678
+ this.diagnostics.fileBasedRouter = {
2679
+ kind: "expo-router",
2680
+ routeDir: expo.routeDir,
2681
+ routes: expo.routes.length
2682
+ };
2683
+ const byNavigator = /* @__PURE__ */ new Map();
2684
+ for (const nav of expo.navigators) {
2685
+ byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
2686
+ }
2687
+ for (const route of expo.routes) {
2688
+ const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
2689
+ name: route.navigatorName,
2690
+ type: route.navigatorType,
2691
+ screens: []
2692
+ }).get(route.navigatorName);
2693
+ navigator.screens.push({
2694
+ name: route.name,
2695
+ navigatorName: route.navigatorName,
2696
+ navigatorType: route.navigatorType,
2697
+ componentFile: route.componentFile,
2698
+ ...route.params.length ? {
2699
+ params: route.params.map((name) => ({ name, type: "string", required: true }))
2700
+ } : {}
2701
+ });
2702
+ this.routeTargets.set(route.name, route.componentFile);
2703
+ }
2704
+ parsedNavigators.push(...byNavigator.values());
2705
+ }
2706
+ let staticOnly = 0;
2707
+ for (const decl of declarations.values()) {
2708
+ if (!decl.confident || !decl.staticScreens?.length) continue;
2709
+ const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
2710
+ if (existing) {
2711
+ const seen = new Set(existing.screens.map((s) => s.name));
2712
+ for (const screen of decl.staticScreens) {
2713
+ if (!seen.has(screen.name)) existing.screens.push(screen);
2714
+ }
2715
+ continue;
2716
+ }
2717
+ parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
2718
+ staticOnly++;
2719
+ }
2720
+ const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
2721
+ const uniqueRoutes = new Set(
2722
+ parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
2723
+ );
2724
+ this.diagnostics = {
2725
+ ...this.diagnostics,
2726
+ filesScanned: navigationFiles.length,
2727
+ filesWithEvidence: evidenceFiles.length,
2728
+ navigatorsDeclared: confidentDeclarations,
2729
+ navigatorsWithJsx: parsedNavigators.length,
2730
+ routes: uniqueRoutes.size,
2731
+ // `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
2732
+ // registrada por um navegador que não sobreviveu. O mínimo evita a
2733
+ // cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
2734
+ routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
2735
+ };
2736
+ console.log(
2737
+ `[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`
2738
+ );
1663
2739
  this.attachParamsToNavigators(parsedNavigators, typeExports);
1664
2740
  return this.buildNavigationGraph(parsedNavigators);
1665
2741
  }
1666
- /** Find all navigation-related files */
2742
+ /** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
2743
+ collectDeclarations(filePath) {
2744
+ const ast = this.graph.parse(filePath);
2745
+ if (!ast) return [];
2746
+ const out = /* @__PURE__ */ new Map();
2747
+ traverse5(ast, {
2748
+ CallExpression: (nodePath) => {
2749
+ const callee = nodePath.node.callee;
2750
+ if (!BabelTypes.isIdentifier(callee)) return;
2751
+ const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
2752
+ const type = known ?? inferNavigatorType(callee.name);
2753
+ const confident = known !== null;
2754
+ let up = nodePath.parentPath;
2755
+ for (let i = 0; i < 4 && up; i++) {
2756
+ if (BabelTypes.isVariableDeclarator(up.node)) break;
2757
+ if (!BabelTypes.isMemberExpression(up.node) && !BabelTypes.isCallExpression(up.node)) {
2758
+ up = null;
2759
+ break;
2760
+ }
2761
+ up = up.parentPath;
2762
+ }
2763
+ const declarator = up?.node;
2764
+ if (!declarator || !BabelTypes.isVariableDeclarator(declarator) || !BabelTypes.isIdentifier(declarator.id)) {
2765
+ return;
2766
+ }
2767
+ const name = declarator.id.name;
2768
+ if (out.get(name)?.confident) return;
2769
+ const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
2770
+ out.set(name, {
2771
+ file: filePath,
2772
+ name,
2773
+ type,
2774
+ confident,
2775
+ ...staticScreens.length ? { staticScreens } : {}
2776
+ });
2777
+ }
2778
+ });
2779
+ return [...out.values()];
2780
+ }
2781
+ /**
2782
+ * As rotas de `createXNavigator({ screens: { … } })`.
2783
+ *
2784
+ * Quatro formas no corpus, todas em `rocketchat`:
2785
+ * `NewServerView` — shorthand, nome = componente
2786
+ * `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
2787
+ * `SelectListView: SelectListViewScreen` — identificador direto
2788
+ * `ChatsStackNavigator: ChatsStack` — navegador aninhado
2789
+ *
2790
+ * `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
2791
+ * e agrupar rotas não muda o que elas são.
2792
+ */
2793
+ parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
2794
+ if (!config || !BabelTypes.isObjectExpression(config) || depth > 4) return [];
2795
+ const out = [];
2796
+ for (const prop of config.properties) {
2797
+ if (!BabelTypes.isObjectProperty(prop)) continue;
2798
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
2799
+ if (!key) continue;
2800
+ if (key === "groups" && BabelTypes.isObjectExpression(prop.value)) {
2801
+ for (const group of prop.value.properties) {
2802
+ if (!BabelTypes.isObjectProperty(group) || !BabelTypes.isExpression(group.value)) continue;
2803
+ out.push(
2804
+ ...this.parseStaticScreens(
2805
+ filePath,
2806
+ group.value,
2807
+ navigatorName,
2808
+ navigatorType,
2809
+ depth + 1
2810
+ )
2811
+ );
2812
+ }
2813
+ continue;
2814
+ }
2815
+ if (key !== "screens" || !BabelTypes.isObjectExpression(prop.value)) continue;
2816
+ for (const entry of prop.value.properties) {
2817
+ if (!BabelTypes.isObjectProperty(entry)) continue;
2818
+ const routeName = BabelTypes.isIdentifier(entry.key) ? entry.key.name : BabelTypes.isStringLiteral(entry.key) ? entry.key.value : null;
2819
+ if (!routeName) continue;
2820
+ const componentExpr = unwrapStaticScreen(entry.value);
2821
+ const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
2822
+ if (componentFile) this.routeTargets.set(routeName, componentFile);
2823
+ out.push({
2824
+ name: routeName,
2825
+ navigatorName,
2826
+ navigatorType,
2827
+ ...componentFile ? { componentFile } : {}
2828
+ });
2829
+ }
2830
+ }
2831
+ return out;
2832
+ }
2833
+ /**
2834
+ * Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
2835
+ * certo.
2836
+ *
2837
+ * POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
2838
+ * `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
2839
+ * convenção de caminho. Medido contra 20 apps React Native de terceiros
2840
+ * (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
2841
+ * app não ter navegação:
2842
+ *
2843
+ * - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
2844
+ * - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
2845
+ * sensível a caixa;
2846
+ * - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
2847
+ * pedia um arquivo chamado `routes*`;
2848
+ * - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
2849
+ * - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
2850
+ *
2851
+ * Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
2852
+ * globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
2853
+ * O que segura o custo aqui é o portão por evidência em `analyze()`, que só
2854
+ * parseia arquivo cujo texto menciona uma fábrica de navegador.
2855
+ *
2856
+ * Os globs antigos continuam na lista: se um projeto restringir `include`, o
2857
+ * que era encontrado antes continua sendo.
2858
+ */
1667
2859
  async findNavigationFiles() {
1668
2860
  const patterns = [
2861
+ ...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
2862
+ // Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
1669
2863
  "**/navigation/**/*.{ts,tsx}",
1670
2864
  "**/navigator*.{ts,tsx}",
1671
2865
  "**/routes*.{ts,tsx}",
@@ -1676,91 +2870,188 @@ var NavigationAnalyzer = class {
1676
2870
  "**/node_modules/**",
1677
2871
  "**/dist/**",
1678
2872
  "**/build/**",
2873
+ // Um navegador declarado dentro de um teste é fixture, não a navegação
2874
+ // do app. Isto passou a importar quando a varredura deixou de ser por
2875
+ // caminho: em `expensify`, os únicos arquivos que o glob antigo
2876
+ // alcançava eram três de `tests/`.
2877
+ "**/__tests__/**",
2878
+ "**/test/**",
2879
+ "**/tests/**",
2880
+ "**/*.test.{ts,tsx,js,jsx}",
2881
+ "**/*.tests.{ts,tsx,js,jsx}",
2882
+ "**/*.spec.{ts,tsx,js,jsx}",
2883
+ // Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
2884
+ // `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
2885
+ // Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
2886
+ // destinos que existem no teste e não no app, para os quais o agente
2887
+ // tentaria navegar.
2888
+ "**/*Test.{ts,tsx,js,jsx}",
2889
+ "**/*Tests.{ts,tsx,js,jsx}",
1679
2890
  ...this.config.exclude || [],
1680
2891
  // §5: Add user-configured navigation excludes
1681
2892
  ...this.navigationExclude
1682
2893
  ];
1683
- const files = await glob(patterns, {
2894
+ const files = await globSorted(patterns, {
1684
2895
  cwd: this.config.rootDir,
1685
2896
  ignore: excludePatterns
1686
2897
  });
1687
- return files.map((file) => path8__default.join(this.config.rootDir, file));
2898
+ return files.map((file) => path2__default.join(this.config.rootDir, file));
1688
2899
  }
1689
- /** Parse navigator definitions from a file */
1690
- parseNavigators(content, filePath) {
1691
- const navigators = [];
1692
- try {
1693
- const ast = parser.parse(content, {
1694
- sourceType: "module",
1695
- plugins: [
1696
- "jsx",
1697
- "typescript",
1698
- ...this.config.parserPlugins || []
1699
- ]
1700
- });
1701
- const navigatorCalls = /* @__PURE__ */ new Map();
1702
- const screensByNavigator = /* @__PURE__ */ new Map();
1703
- traverse4(ast, {
1704
- // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
1705
- CallExpression: (nodePath) => {
1706
- const { node } = nodePath;
1707
- const callee = node.callee;
1708
- let navigatorType = null;
1709
- if (BabelTypes.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
1710
- navigatorType = "stack";
1711
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createStackNavigator") {
1712
- navigatorType = "stack";
1713
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
1714
- navigatorType = "tab";
1715
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createTabNavigator") {
1716
- navigatorType = "tab";
1717
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
1718
- navigatorType = "drawer";
1719
- }
1720
- if (navigatorType) {
1721
- const parent = nodePath.parent;
1722
- if (BabelTypes.isVariableDeclarator(parent) && BabelTypes.isIdentifier(parent.id)) {
1723
- const varName = parent.id.name;
1724
- navigatorCalls.set(varName, {
1725
- name: varName,
1726
- type: navigatorType,
1727
- screens: []
1728
- });
1729
- }
1730
- }
1731
- },
1732
- // Detect Stack.Navigator / Tab.Navigator JSX elements
1733
- JSXElement: (nodePath) => {
1734
- const { node } = nodePath;
1735
- const openingElement = node.openingElement;
1736
- if (BabelTypes.isJSXMemberExpression(openingElement.name) && BabelTypes.isJSXIdentifier(openingElement.name.object)) {
1737
- const objectName = openingElement.name.object.name;
1738
- const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
1739
- if (propertyName === "Navigator") {
1740
- const navigator = navigatorCalls.get(objectName);
1741
- if (navigator) {
1742
- const initialRouteAttr = openingElement.attributes.find(
1743
- (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
1744
- );
1745
- if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
1746
- navigator.initialRouteName = initialRouteAttr.value.value;
1747
- }
1748
- const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
1749
- screensByNavigator.set(objectName, screens);
1750
- }
1751
- }
1752
- }
2900
+ /**
2901
+ * Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
2902
+ * que pode estar aqui ou em qualquer arquivo que este importe.
2903
+ */
2904
+ parseNavigatorUsages(filePath, declarations) {
2905
+ const ast = this.graph.parse(filePath);
2906
+ if (!ast) return [];
2907
+ const found = /* @__PURE__ */ new Map();
2908
+ traverse5(ast, {
2909
+ JSXElement: (nodePath) => {
2910
+ const { node } = nodePath;
2911
+ const openingElement = node.openingElement;
2912
+ if (!BabelTypes.isJSXMemberExpression(openingElement.name) || !BabelTypes.isJSXIdentifier(openingElement.name.object)) {
2913
+ return;
1753
2914
  }
1754
- });
1755
- navigatorCalls.forEach((navigator) => {
1756
- const screens = screensByNavigator.get(navigator.name) || [];
1757
- navigator.screens = screens;
1758
- navigators.push(navigator);
1759
- });
1760
- } catch (error) {
1761
- console.warn(`Failed to parse navigators in ${filePath}:`, error);
2915
+ const objectName = openingElement.name.object.name;
2916
+ const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2917
+ if (propertyName !== "Navigator") return;
2918
+ const decl = this.resolveNavigator(filePath, objectName, declarations);
2919
+ if (!decl) return;
2920
+ const navigator = found.get(objectName) ?? {
2921
+ name: objectName,
2922
+ type: decl.type,
2923
+ screens: []
2924
+ };
2925
+ const initialRouteAttr = openingElement.attributes.find(
2926
+ (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2927
+ );
2928
+ if (BabelTypes.isJSXAttribute(initialRouteAttr)) {
2929
+ const initial = this.attributeString(filePath, initialRouteAttr);
2930
+ if (initial) navigator.initialRouteName = initial;
2931
+ }
2932
+ this.navigatorFiles.add(filePath);
2933
+ this.navigatorFiles.add(decl.file);
2934
+ navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
2935
+ found.set(objectName, navigator);
2936
+ }
2937
+ });
2938
+ return [...found.values()];
2939
+ }
2940
+ /**
2941
+ * Rotas declaradas FORA de qualquer `<X.Navigator>`.
2942
+ *
2943
+ * O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
2944
+ * que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
2945
+ * dois são 171 rotas invisíveis:
2946
+ *
2947
+ * `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
2948
+ * <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
2949
+ * dentro de seis navegadores diferentes. 70 rotas.
2950
+ * `comapeo` — `export const createAppScreens = ({intl}) => (<>
2951
+ * <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
2952
+ * num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
2953
+ *
2954
+ * A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
2955
+ * navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
2956
+ * literal do fonte (ou constante que o resolvedor segue), e `X` precisa
2957
+ * chegar a uma declaração de navegador que já existe. O que não resolve não
2958
+ * vira nada.
2959
+ *
2960
+ * O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
2961
+ * fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
2962
+ *
2963
+ * DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
2964
+ * primeira é a de sempre (declaração local, ou `import` seguido até a
2965
+ * origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
2966
+ * parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
2967
+ * o navegador `Flat` é o próprio app, no fonte.
2968
+ *
2969
+ * O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
2970
+ * contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
2971
+ * cinco stacks que também chamam a mesma função. Seguir os seis pontos de
2972
+ * chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
2973
+ * da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
2974
+ */
2975
+ parseDetachedScreens(filePath, declarations) {
2976
+ const ast = this.graph.parse(filePath);
2977
+ if (!ast) return [];
2978
+ const byNavigator = /* @__PURE__ */ new Map();
2979
+ const resolved = /* @__PURE__ */ new Map();
2980
+ traverse5(ast, {
2981
+ JSXElement: (nodePath) => {
2982
+ const name = nodePath.node.openingElement.name;
2983
+ if (!BabelTypes.isJSXMemberExpression(name) || !BabelTypes.isJSXIdentifier(name.object)) return;
2984
+ if (!BabelTypes.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
2985
+ const insideNavigator = nodePath.findParent((parent) => {
2986
+ if (!parent.isJSXElement()) return false;
2987
+ const parentName = parent.node.openingElement.name;
2988
+ return BabelTypes.isJSXMemberExpression(parentName) && BabelTypes.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
2989
+ });
2990
+ if (insideNavigator) return;
2991
+ const local = name.object.name;
2992
+ if (!resolved.has(local)) {
2993
+ resolved.set(
2994
+ local,
2995
+ this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
2996
+ );
2997
+ }
2998
+ const decl = resolved.get(local);
2999
+ if (!decl) return;
3000
+ const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
3001
+ if (!screen) return;
3002
+ const nav = byNavigator.get(decl.name) ?? {
3003
+ name: decl.name,
3004
+ type: decl.type,
3005
+ screens: []
3006
+ };
3007
+ if (!nav.screens.some((existing) => existing.name === screen.name))
3008
+ nav.screens.push(screen);
3009
+ byNavigator.set(decl.name, nav);
3010
+ }
3011
+ });
3012
+ return [...byNavigator.values()];
3013
+ }
3014
+ /**
3015
+ * De um `X` usado como `<X.Screen>` até a declaração do navegador.
3016
+ *
3017
+ * Além do caminho normal — declaração local ou `import` seguido até a origem
3018
+ * — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
3019
+ * `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
3020
+ */
3021
+ resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
3022
+ const direct = this.resolveNavigator(filePath, localName, declarations);
3023
+ if (direct) return direct;
3024
+ const binding = nodePath.scope.getBinding(localName);
3025
+ if (!binding || binding.kind !== "param") return null;
3026
+ const param = binding.path.node;
3027
+ if (!BabelTypes.isIdentifier(param) || !BabelTypes.isTSTypeAnnotation(param.typeAnnotation)) return null;
3028
+ const annotation = param.typeAnnotation.typeAnnotation;
3029
+ if (!BabelTypes.isTSTypeQuery(annotation) || !BabelTypes.isIdentifier(annotation.exprName)) return null;
3030
+ return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
3031
+ }
3032
+ /**
3033
+ * De `<X.Navigator>` até a declaração de `X`.
3034
+ *
3035
+ * Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
3036
+ * é definido. Resolver pelo import é o que torna a junção entre arquivos
3037
+ * segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
3038
+ * é o arquivo de DEFINIÇÃO.
3039
+ */
3040
+ resolveNavigator(filePath, localName, declarations) {
3041
+ const local = declarations.get(`${filePath}#${localName}`);
3042
+ if (local) return local;
3043
+ const origin = this.graph.resolveBinding(filePath, localName);
3044
+ if (!origin) return null;
3045
+ return declarations.get(`${origin.file}#${origin.name}`) ?? null;
3046
+ }
3047
+ /** O valor string de um atributo JSX, resolvendo constante importada. */
3048
+ attributeString(filePath, attr) {
3049
+ const value = attr.value;
3050
+ if (BabelTypes.isStringLiteral(value)) return value.value;
3051
+ if (BabelTypes.isJSXExpressionContainer(value) && BabelTypes.isExpression(value.expression)) {
3052
+ return this.graph.stringConstant(filePath, value.expression);
1762
3053
  }
1763
- return navigators;
3054
+ return null;
1764
3055
  }
1765
3056
  /**
1766
3057
  * Is this JSX element `<navigatorVarName.MEMBER …>`?
@@ -1855,22 +3146,48 @@ var NavigationAnalyzer = class {
1855
3146
  return found;
1856
3147
  }
1857
3148
  /** Extract screens from a navigator JSX element */
1858
- extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
3149
+ extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
1859
3150
  const screens = [];
1860
3151
  if (!navigatorElement.children) return screens;
1861
3152
  const seen = /* @__PURE__ */ new Set();
1862
3153
  for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
1863
- const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
1864
- if (!screenName || seen.has(screenName)) continue;
1865
- seen.add(screenName);
1866
- screens.push({
1867
- name: screenName,
1868
- navigatorName: navigatorVarName,
1869
- navigatorType
1870
- });
3154
+ const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
3155
+ if (!screen || seen.has(screen.name)) continue;
3156
+ seen.add(screen.name);
3157
+ screens.push(screen);
1871
3158
  }
1872
3159
  return screens;
1873
3160
  }
3161
+ /**
3162
+ * Uma `<X.Screen>` isolada até a rota que ela declara.
3163
+ *
3164
+ * Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
3165
+ * a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
3166
+ * dele — só a forma de chegar até o elemento muda.
3167
+ */
3168
+ parseScreenElement(filePath, element, navigatorName, navigatorType) {
3169
+ const attrs = element.openingElement.attributes;
3170
+ const nameAttr = attrs.find(
3171
+ (a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "name"
3172
+ );
3173
+ const screenName = BabelTypes.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
3174
+ if (!screenName) return null;
3175
+ const componentAttr = attrs.find(
3176
+ (a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "component"
3177
+ );
3178
+ let componentFile = null;
3179
+ if (BabelTypes.isJSXAttribute(componentAttr) && BabelTypes.isJSXExpressionContainer(componentAttr.value)) {
3180
+ const expr = componentAttr.value.expression;
3181
+ if (BabelTypes.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
3182
+ }
3183
+ if (componentFile) this.routeTargets.set(screenName, componentFile);
3184
+ return {
3185
+ name: screenName,
3186
+ navigatorName,
3187
+ navigatorType,
3188
+ ...componentFile ? { componentFile } : {}
3189
+ };
3190
+ }
1874
3191
  /** Extract string attribute value from JSX attributes */
1875
3192
  extractAttributeValue(attributes, attrName) {
1876
3193
  const attr = attributes.find(
@@ -1893,7 +3210,7 @@ var NavigationAnalyzer = class {
1893
3210
  ...this.config.parserPlugins || []
1894
3211
  ]
1895
3212
  });
1896
- traverse4(ast, {
3213
+ traverse5(ast, {
1897
3214
  TSTypeAliasDeclaration: (nodePath) => {
1898
3215
  const { node } = nodePath;
1899
3216
  const typeName = node.id.name;
@@ -1966,7 +3283,7 @@ var NavigationAnalyzer = class {
1966
3283
  if (type.type === "TSUndefinedKeyword") return "undefined";
1967
3284
  if (type.type === "TSNullKeyword") return "null";
1968
3285
  if (type.type === "TSUnionType") {
1969
- return type.types.map((t12) => this.typeToString(t12)).join(" | ");
3286
+ return type.types.map((t15) => this.typeToString(t15)).join(" | ");
1970
3287
  }
1971
3288
  if (type.type === "TSTypeLiteral") {
1972
3289
  return "object";
@@ -1987,7 +3304,7 @@ var NavigationAnalyzer = class {
1987
3304
  /** Attach parsed type params to navigator screens */
1988
3305
  attachParamsToNavigators(navigators, types) {
1989
3306
  for (const navigator of navigators) {
1990
- const matchingType = types.find((t12) => t12.type === navigator.type);
3307
+ const matchingType = types.find((t15) => t15.type === navigator.type);
1991
3308
  if (matchingType) {
1992
3309
  for (const screen of navigator.screens) {
1993
3310
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -2076,9 +3393,9 @@ var ComponentAnalyzer = class {
2076
3393
  plugins: ["jsx", "typescript", ["decorators", { decoratorsBeforeExport: true }]]
2077
3394
  });
2078
3395
  const components = [];
2079
- traverse4(ast, {
2080
- JSXElement: (path9) => {
2081
- const component = this.extractComponentFromJSXElement(path9.node);
3396
+ traverse5(ast, {
3397
+ JSXElement: (path11) => {
3398
+ const component = this.extractComponentFromJSXElement(path11.node);
2082
3399
  if (component) {
2083
3400
  components.push(component);
2084
3401
  }
@@ -2204,14 +3521,14 @@ var FormAnalyzer = class {
2204
3521
  this.stateVariables.clear();
2205
3522
  this.inputElements = [];
2206
3523
  this.submitButtons = [];
2207
- traverse4(ast, {
2208
- CallExpression: (path9) => {
2209
- this.extractStateVariables(path9.node);
3524
+ traverse5(ast, {
3525
+ CallExpression: (path11) => {
3526
+ this.extractStateVariables(path11.node);
2210
3527
  }
2211
3528
  });
2212
- traverse4(ast, {
2213
- JSXElement: (path9) => {
2214
- this.extractFormElements(path9.node);
3529
+ traverse5(ast, {
3530
+ JSXElement: (path11) => {
3531
+ this.extractFormElements(path11.node);
2215
3532
  }
2216
3533
  });
2217
3534
  const validationRules = this.extractValidationRules(ast);
@@ -2245,23 +3562,23 @@ var FormAnalyzer = class {
2245
3562
  for (const attr of openingElement.attributes) {
2246
3563
  if (BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name)) {
2247
3564
  const propName = attr.name.name;
2248
- const propValue = this.extractAttributeValue(attr.value);
3565
+ const propValue2 = this.extractAttributeValue(attr.value);
2249
3566
  switch (propName) {
2250
3567
  case "label":
2251
- info.label = propValue;
3568
+ info.label = propValue2;
2252
3569
  break;
2253
3570
  case "placeholder":
2254
- info.placeholder = propValue;
3571
+ info.placeholder = propValue2;
2255
3572
  break;
2256
3573
  case "keyboardType":
2257
- info.keyboardType = propValue;
3574
+ info.keyboardType = propValue2;
2258
3575
  break;
2259
3576
  case "testID":
2260
- info.testID = propValue;
3577
+ info.testID = propValue2;
2261
3578
  break;
2262
3579
  case "appilotsId":
2263
- info.appilotsId = propValue;
2264
- if (propValue) info.varName = propValue;
3580
+ info.appilotsId = propValue2;
3581
+ if (propValue2) info.varName = propValue2;
2265
3582
  break;
2266
3583
  case "value":
2267
3584
  if (attr.value && BabelTypes.isJSXExpressionContainer(attr.value) && BabelTypes.isIdentifier(attr.value.expression)) {
@@ -2310,9 +3627,9 @@ var FormAnalyzer = class {
2310
3627
  }
2311
3628
  extractValidationRules(ast) {
2312
3629
  const rules = {};
2313
- traverse4(ast, {
2314
- IfStatement: (path9) => {
2315
- const test = path9.node.test;
3630
+ traverse5(ast, {
3631
+ IfStatement: (path11) => {
3632
+ const test = path11.node.test;
2316
3633
  const rule = this.extractRuleFromCondition(test);
2317
3634
  if (rule) {
2318
3635
  const { field, description } = rule;
@@ -2366,7 +3683,7 @@ var FormAnalyzer = class {
2366
3683
  }
2367
3684
  buildForms(filePath, validationRules) {
2368
3685
  if (this.inputElements.length === 0) return [];
2369
- const fileName = path8.basename(filePath, path8.extname(filePath));
3686
+ const fileName = path2.basename(filePath, path2.extname(filePath));
2370
3687
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2371
3688
  const fields = this.inputElements.map((input) => {
2372
3689
  const fieldType = this.inferFieldType(input);
@@ -2415,13 +3732,105 @@ var FormAnalyzer = class {
2415
3732
  return rule !== void 0 && rule.includes("required");
2416
3733
  }
2417
3734
  };
3735
+
3736
+ // src/pipeline/composition.ts
3737
+ var MAX_DEPTH = 3;
3738
+ var MAX_VISITED_PER_SCREEN = 60;
3739
+ var MAX_FAN_IN = 3;
3740
+ var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
3741
+ function descendants(graph, from, screenFiles) {
3742
+ const seen = /* @__PURE__ */ new Set([from]);
3743
+ const out = [];
3744
+ let frontier = [from];
3745
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
3746
+ const next = [];
3747
+ for (const file of frontier) {
3748
+ for (const child of graph.renderedComponentFiles(file)) {
3749
+ if (seen.has(child)) continue;
3750
+ seen.add(child);
3751
+ if (screenFiles.has(child)) continue;
3752
+ out.push(child);
3753
+ next.push(child);
3754
+ if (out.length >= MAX_VISITED_PER_SCREEN) return out;
3755
+ }
3756
+ }
3757
+ frontier = next;
3758
+ }
3759
+ return out;
3760
+ }
3761
+ function addressable(locator) {
3762
+ if (!locator || typeof locator !== "object") return false;
3763
+ const l = locator;
3764
+ if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
3765
+ return typeof l["testID"] === "string" && l["testID"].length > 0;
3766
+ }
3767
+ async function composeAffordances(options) {
3768
+ const { screens, graph, analyzeFile, screenFiles } = options;
3769
+ const byScreen = /* @__PURE__ */ new Map();
3770
+ const fanIn = /* @__PURE__ */ new Map();
3771
+ for (const screen of screens) {
3772
+ if (!screen.filePath) continue;
3773
+ const children = descendants(graph, screen.filePath, screenFiles);
3774
+ byScreen.set(screen, children);
3775
+ for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
3776
+ }
3777
+ const analyzed = /* @__PURE__ */ new Map();
3778
+ let screensEnriched = 0;
3779
+ let filesMerged = 0;
3780
+ for (const [screen, children] of byScreen) {
3781
+ const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
3782
+ if (exclusive.length === 0) continue;
3783
+ const actionIds = new Set(screen.actions.map((a) => a.id));
3784
+ const targetIds = new Set((screen.targets ?? []).map((t15) => t15.id));
3785
+ const formIds = new Set(screen.forms.map((f) => f.id));
3786
+ let gained = false;
3787
+ for (const file of exclusive) {
3788
+ if (!analyzed.has(file)) {
3789
+ try {
3790
+ analyzed.set(file, await analyzeFile(file));
3791
+ } catch {
3792
+ analyzed.set(file, null);
3793
+ }
3794
+ }
3795
+ const child = analyzed.get(file);
3796
+ if (!child) continue;
3797
+ let merged = false;
3798
+ for (const action of child.actions) {
3799
+ if (!addressable(action.locator)) continue;
3800
+ if (actionIds.has(action.id)) continue;
3801
+ actionIds.add(action.id);
3802
+ screen.actions.push(action);
3803
+ merged = true;
3804
+ }
3805
+ for (const target of child.targets ?? []) {
3806
+ if (!addressable(target.locator)) continue;
3807
+ if (targetIds.has(target.id)) continue;
3808
+ targetIds.add(target.id);
3809
+ (screen.targets ??= []).push(target);
3810
+ merged = true;
3811
+ }
3812
+ for (const form of child.forms) {
3813
+ if (form.fields.length === 0) continue;
3814
+ const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
3815
+ if (formIds.has(id)) continue;
3816
+ formIds.add(id);
3817
+ screen.forms.push({ ...form, id });
3818
+ merged = true;
3819
+ }
3820
+ if (merged) {
3821
+ filesMerged++;
3822
+ gained = true;
3823
+ }
3824
+ }
3825
+ if (gained) screensEnriched++;
3826
+ }
3827
+ return { screens, screensEnriched, filesMerged };
3828
+ }
3829
+
3830
+ // src/analyzers/ReactNativePlatformAnalyzer.ts
2418
3831
  var ReactNativePlatformAnalyzer = class {
2419
3832
  platform = "react-native";
2420
3833
  async analyze(config, options) {
2421
- const screenAnalyzer = new ScreenAnalyzer(config, {
2422
- strictScreens: options.strictScreens ?? true,
2423
- screenPatterns: options.screenPatterns
2424
- });
2425
3834
  const navigationAnalyzer = new NavigationAnalyzer(config, {
2426
3835
  navigationInclude: options.navigationInclude,
2427
3836
  navigationExclude: options.navigationExclude
@@ -2429,14 +3838,31 @@ var ReactNativePlatformAnalyzer = class {
2429
3838
  const componentAnalyzer = new ComponentAnalyzer(config);
2430
3839
  const formAnalyzer = new FormAnalyzer(config);
2431
3840
  console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
2432
- const [screens, navigation] = await Promise.all([
2433
- screenAnalyzer.analyze(),
2434
- navigationAnalyzer.analyze()
2435
- ]);
3841
+ const navigation = await navigationAnalyzer.analyze();
3842
+ const screenAnalyzer = new ScreenAnalyzer(config, {
3843
+ strictScreens: options.strictScreens ?? true,
3844
+ screenPatterns: options.screenPatterns,
3845
+ // Menos os arquivos que declaram navegador: ver `navigatorFiles`.
3846
+ routeTargetFiles: new Set(
3847
+ [...navigationAnalyzer.routeTargets.values()].filter(
3848
+ (file) => !navigationAnalyzer.navigatorFiles.has(file)
3849
+ )
3850
+ )
3851
+ });
3852
+ const screens = await screenAnalyzer.analyze();
3853
+ const composed = await composeAffordances({
3854
+ screens,
3855
+ graph: navigationAnalyzer.graph,
3856
+ analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
3857
+ screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
3858
+ });
3859
+ console.log(
3860
+ `[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
3861
+ );
2436
3862
  console.log(
2437
3863
  `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
2438
3864
  );
2439
- const screenFiles = await glob(config.include || ["**/*.tsx", "**/*.ts"], {
3865
+ const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
2440
3866
  cwd: config.rootDir,
2441
3867
  ignore: config.exclude || ["**/node_modules/**"]
2442
3868
  });
@@ -2444,7 +3870,7 @@ var ReactNativePlatformAnalyzer = class {
2444
3870
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
2445
3871
  );
2446
3872
  const enrichmentPromises = screenFiles.map(async (file) => {
2447
- const filePath = path8__default.resolve(config.rootDir, file);
3873
+ const filePath = path2__default.resolve(config.rootDir, file);
2448
3874
  try {
2449
3875
  const [components, forms] = await Promise.all([
2450
3876
  componentAnalyzer.analyzeFile(filePath),
@@ -2497,9 +3923,11 @@ var ReactNativePlatformAnalyzer = class {
2497
3923
  });
2498
3924
  return {
2499
3925
  screens: enrichedScreens,
3926
+ controlEvidenceFiles: screenAnalyzer.controlEvidenceFiles,
2500
3927
  navigation,
2501
3928
  analyzedFiles: screenFiles.length,
2502
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
3929
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
3930
+ diagnostics: { navigation: navigationAnalyzer.diagnostics }
2503
3931
  };
2504
3932
  }
2505
3933
  mergeForm(target, source) {
@@ -2517,9 +3945,12 @@ var ReactNativePlatformAnalyzer = class {
2517
3945
  findEquivalentField(fields, incoming) {
2518
3946
  return fields.find((field) => {
2519
3947
  if (field.name && incoming.name && field.name === incoming.name) return true;
2520
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2521
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2522
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
3948
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
3949
+ return true;
3950
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
3951
+ return true;
3952
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
3953
+ return true;
2523
3954
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2524
3955
  return true;
2525
3956
  }
@@ -2552,7 +3983,9 @@ var ReactNativePlatformAnalyzer = class {
2552
3983
  if (incoming.fields.length === 0) return void 0;
2553
3984
  let best;
2554
3985
  for (const form of forms) {
2555
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
3986
+ const overlap = incoming.fields.filter(
3987
+ (field) => this.findEquivalentField(form.fields, field)
3988
+ ).length;
2556
3989
  if (overlap > 0 && (!best || overlap > best.overlap)) {
2557
3990
  best = { form, overlap };
2558
3991
  }
@@ -2706,13 +4139,13 @@ function staticRoutePath(node) {
2706
4139
  if (!node) return void 0;
2707
4140
  if (BabelTypes.isStringLiteral(node)) return node.value;
2708
4141
  if (BabelTypes.isTemplateLiteral(node)) {
2709
- let path9 = "";
4142
+ let path11 = "";
2710
4143
  node.quasis.forEach((quasi, index) => {
2711
- path9 += quasi.value.cooked ?? quasi.value.raw;
4144
+ path11 += quasi.value.cooked ?? quasi.value.raw;
2712
4145
  const expr = node.expressions[index];
2713
- if (expr) path9 += `:${paramNameOf(expr)}`;
4146
+ if (expr) path11 += `:${paramNameOf(expr)}`;
2714
4147
  });
2715
- return path9;
4148
+ return path11;
2716
4149
  }
2717
4150
  return void 0;
2718
4151
  }
@@ -2755,7 +4188,7 @@ function extractWebNavigationCalls(ast) {
2755
4188
  calls.push({ method: "navigate", targetPath: node.right.value });
2756
4189
  }
2757
4190
  };
2758
- traverse4(ast, {
4191
+ traverse5(ast, {
2759
4192
  noScope: !BabelTypes.isFile(ast),
2760
4193
  enter: (nodePath) => inspect(nodePath.node)
2761
4194
  });
@@ -2797,17 +4230,17 @@ var WebScreenAnalyzer = class {
2797
4230
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2798
4231
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2799
4232
  } = this.config;
2800
- const files = (await glob(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
4233
+ const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2801
4234
  (file) => !file.endsWith(".d.ts")
2802
4235
  );
2803
- const patternMatches = await glob(this.screenPatterns, {
4236
+ const patternMatches = await globSorted(this.screenPatterns, {
2804
4237
  cwd: this.config.rootDir,
2805
4238
  ignore: exclude
2806
4239
  });
2807
- const patternSet = new Set(patternMatches.map((f) => path8__default.resolve(this.config.rootDir, f)));
4240
+ const patternSet = new Set(patternMatches.map((f) => path2__default.resolve(this.config.rootDir, f)));
2808
4241
  const candidates = [];
2809
4242
  for (const file of files) {
2810
- const filePath = path8__default.resolve(this.config.rootDir, file);
4243
+ const filePath = path2__default.resolve(this.config.rootDir, file);
2811
4244
  try {
2812
4245
  const candidate = await this.analyzeFile(filePath);
2813
4246
  if (!candidate) continue;
@@ -2851,7 +4284,7 @@ var WebScreenAnalyzer = class {
2851
4284
  }
2852
4285
  }
2853
4286
  }
2854
- const name = registerScreenMeta?.name || componentName || path8__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
4287
+ const name = registerScreenMeta?.name || componentName || path2__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
2855
4288
  const descriptor = {
2856
4289
  name,
2857
4290
  filePath,
@@ -2877,7 +4310,7 @@ var WebScreenAnalyzer = class {
2877
4310
  // ── registerScreen ────────────────────────────────────────────────
2878
4311
  detectRegisterScreenCall(ast) {
2879
4312
  let found = false;
2880
- traverse4(ast, {
4313
+ traverse5(ast, {
2881
4314
  CallExpression: (nodePath) => {
2882
4315
  if (found) return;
2883
4316
  if (isRegisterScreenCallee(nodePath.node.callee)) {
@@ -2890,7 +4323,7 @@ var WebScreenAnalyzer = class {
2890
4323
  }
2891
4324
  extractRegisterScreenMetadata(ast) {
2892
4325
  let plain = null;
2893
- traverse4(ast, {
4326
+ traverse5(ast, {
2894
4327
  CallExpression: (nodePath) => {
2895
4328
  if (!isRegisterScreenCallee(nodePath.node.callee)) return;
2896
4329
  const arg = nodePath.node.arguments[0];
@@ -2938,7 +4371,7 @@ var WebScreenAnalyzer = class {
2938
4371
  extractComponentName(ast) {
2939
4372
  let defaultName = "";
2940
4373
  let firstExported = "";
2941
- traverse4(ast, {
4374
+ traverse5(ast, {
2942
4375
  ExportDefaultDeclaration: (nodePath) => {
2943
4376
  const declaration = nodePath.node.declaration;
2944
4377
  if (BabelTypes.isFunctionDeclaration(declaration) && declaration.id?.name) {
@@ -2967,7 +4400,7 @@ var WebScreenAnalyzer = class {
2967
4400
  extractComponents(ast) {
2968
4401
  const components = [];
2969
4402
  const seen = /* @__PURE__ */ new Set();
2970
- traverse4(ast, {
4403
+ traverse5(ast, {
2971
4404
  JSXOpeningElement: (nodePath) => {
2972
4405
  const element = nodePath.node;
2973
4406
  const name = getJsxElementName(element);
@@ -2988,7 +4421,7 @@ var WebScreenAnalyzer = class {
2988
4421
  // ── <label htmlFor> association ───────────────────────────────────
2989
4422
  collectHtmlForLabels(ast) {
2990
4423
  const labels = /* @__PURE__ */ new Map();
2991
- traverse4(ast, {
4424
+ traverse5(ast, {
2992
4425
  JSXElement: (nodePath) => {
2993
4426
  const element = nodePath.node;
2994
4427
  if (getJsxElementName(element.openingElement) !== "label") return;
@@ -3031,7 +4464,7 @@ var WebScreenAnalyzer = class {
3031
4464
  formBuckets.set(formElement, bucket);
3032
4465
  return bucket;
3033
4466
  };
3034
- traverse4(ast, {
4467
+ traverse5(ast, {
3035
4468
  JSXElement: (nodePath) => {
3036
4469
  const element = nodePath.node;
3037
4470
  const name = getJsxElementName(element.openingElement);
@@ -3048,7 +4481,7 @@ var WebScreenAnalyzer = class {
3048
4481
  if (!bucket.fields.has(field.name)) bucket.fields.set(field.name, field);
3049
4482
  }
3050
4483
  });
3051
- traverse4(ast, {
4484
+ traverse5(ast, {
3052
4485
  JSXElement: (nodePath) => {
3053
4486
  const element = nodePath.node;
3054
4487
  const name = getJsxElementName(element.openingElement);
@@ -3226,7 +4659,7 @@ var WebScreenAnalyzer = class {
3226
4659
  const actionLabels = new Map(
3227
4660
  actions.filter((a) => a.label).map((a) => [normalizeLabel(a.label), a])
3228
4661
  );
3229
- traverse4(ast, {
4662
+ traverse5(ast, {
3230
4663
  JSXElement: (nodePath) => {
3231
4664
  const element = nodePath.node;
3232
4665
  const name = getJsxElementName(element.openingElement);
@@ -3457,7 +4890,7 @@ var WebScreenAnalyzer = class {
3457
4890
  for (const call of extractWebNavigationCalls(ast)) {
3458
4891
  if (call.targetPath && call.targetPath.startsWith("/")) targets.add(call.targetPath);
3459
4892
  }
3460
- traverse4(ast, {
4893
+ traverse5(ast, {
3461
4894
  JSXOpeningElement: (nodePath) => {
3462
4895
  const element = nodePath.node;
3463
4896
  const name = getJsxElementName(element);
@@ -3477,7 +4910,7 @@ var WebScreenAnalyzer = class {
3477
4910
  extractCollections(ast) {
3478
4911
  const collections = [];
3479
4912
  const seen = /* @__PURE__ */ new Set();
3480
- traverse4(ast, {
4913
+ traverse5(ast, {
3481
4914
  JSXExpressionContainer: (nodePath) => {
3482
4915
  const expr = nodePath.node.expression;
3483
4916
  if (!BabelTypes.isCallExpression(expr) || !BabelTypes.isMemberExpression(expr.callee) || !BabelTypes.isIdentifier(expr.callee.object) || !BabelTypes.isIdentifier(expr.callee.property) || expr.callee.property.name !== "map") {
@@ -3589,7 +5022,7 @@ function firstCalledFunctionName(node) {
3589
5022
  }
3590
5023
  function containsWindowConfirm(body) {
3591
5024
  let found = false;
3592
- traverse4(
5025
+ traverse5(
3593
5026
  body,
3594
5027
  {
3595
5028
  noScope: true,
@@ -3641,7 +5074,7 @@ function collectionItemNames(callback) {
3641
5074
  function collectionDisplayFields(callback, itemNames) {
3642
5075
  const fields = /* @__PURE__ */ new Set();
3643
5076
  if (!callback.body) return [];
3644
- traverse4(
5077
+ traverse5(
3645
5078
  callback.body,
3646
5079
  {
3647
5080
  noScope: true,
@@ -3658,7 +5091,7 @@ function collectionDisplayFields(callback, itemNames) {
3658
5091
  function collectionKeyField(callback, itemNames) {
3659
5092
  let keyField;
3660
5093
  if (!callback.body) return void 0;
3661
- traverse4(
5094
+ traverse5(
3662
5095
  callback.body,
3663
5096
  {
3664
5097
  noScope: true,
@@ -3740,7 +5173,9 @@ var WebNavigationAnalyzer = class {
3740
5173
  for (const filePath of files) {
3741
5174
  try {
3742
5175
  const content = await promises.readFile(filePath, "utf-8");
3743
- if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
5176
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
5177
+ content
5178
+ )) {
3744
5179
  continue;
3745
5180
  }
3746
5181
  const ast = parseSource(content, this.config.parserPlugins);
@@ -3770,8 +5205,8 @@ var WebNavigationAnalyzer = class {
3770
5205
  ...this.config.exclude || [],
3771
5206
  ...this.navigationExclude
3772
5207
  ];
3773
- const files = await glob(patterns, { cwd: this.config.rootDir, ignore });
3774
- return files.map((file) => path8__default.join(this.config.rootDir, file));
5208
+ const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
5209
+ return files.map((file) => path2__default.join(this.config.rootDir, file));
3775
5210
  }
3776
5211
  // ── JSX <Route> style ────────────────────────────────────────────
3777
5212
  extractJsxRoutes(ast) {
@@ -3799,7 +5234,7 @@ var WebNavigationAnalyzer = class {
3799
5234
  if (BabelTypes.isJSXElement(child)) visitRoute(child, fullPath);
3800
5235
  }
3801
5236
  };
3802
- traverse4(ast, {
5237
+ traverse5(ast, {
3803
5238
  JSXElement: (nodePath) => {
3804
5239
  const name = getJsxElementName(nodePath.node.openingElement);
3805
5240
  if (name !== "Routes" && name !== "Route") return;
@@ -3838,7 +5273,7 @@ var WebNavigationAnalyzer = class {
3838
5273
  "createMemoryRouter",
3839
5274
  "useRoutes"
3840
5275
  ]);
3841
- traverse4(ast, {
5276
+ traverse5(ast, {
3842
5277
  CallExpression: (nodePath) => {
3843
5278
  const callee = nodePath.node.callee;
3844
5279
  if (!BabelTypes.isIdentifier(callee) || !ROUTER_FACTORIES.has(callee.name)) return;
@@ -3943,11 +5378,13 @@ var WebNavigationAnalyzer = class {
3943
5378
  return {
3944
5379
  screens,
3945
5380
  initialScreen: initialRoute?.screenName ?? "",
3946
- navigators: routes.length > 0 ? [{
3947
- name: navigatorName,
3948
- type: WEB_NAVIGATOR_TYPE,
3949
- screens: screenNames
3950
- }] : []
5381
+ navigators: routes.length > 0 ? [
5382
+ {
5383
+ name: navigatorName,
5384
+ type: WEB_NAVIGATOR_TYPE,
5385
+ screens: screenNames
5386
+ }
5387
+ ] : []
3951
5388
  };
3952
5389
  }
3953
5390
  };
@@ -4269,8 +5706,8 @@ var ZodParsedType = util.arrayToEnum([
4269
5706
  "set"
4270
5707
  ]);
4271
5708
  var getParsedType = (data) => {
4272
- const t12 = typeof data;
4273
- switch (t12) {
5709
+ const t15 = typeof data;
5710
+ switch (t15) {
4274
5711
  case "undefined":
4275
5712
  return ZodParsedType.undefined;
4276
5713
  case "string":
@@ -4542,8 +5979,8 @@ function getErrorMap() {
4542
5979
 
4543
5980
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4544
5981
  var makeIssue = (params) => {
4545
- const { data, path: path9, errorMaps, issueData } = params;
4546
- const fullPath = [...path9, ...issueData.path || []];
5982
+ const { data, path: path11, errorMaps, issueData } = params;
5983
+ const fullPath = [...path11, ...issueData.path || []];
4547
5984
  const fullIssue = {
4548
5985
  ...issueData,
4549
5986
  path: fullPath
@@ -4659,11 +6096,11 @@ var errorUtil;
4659
6096
 
4660
6097
  // ../../node_modules/zod/v3/types.js
4661
6098
  var ParseInputLazyPath = class {
4662
- constructor(parent, value, path9, key) {
6099
+ constructor(parent, value, path11, key) {
4663
6100
  this._cachedPath = [];
4664
6101
  this.parent = parent;
4665
6102
  this.data = value;
4666
- this._path = path9;
6103
+ this._path = path11;
4667
6104
  this._key = key;
4668
6105
  }
4669
6106
  get path() {
@@ -8104,7 +9541,36 @@ var coerce = {
8104
9541
  };
8105
9542
  var NEVER = INVALID;
8106
9543
 
8107
- // ../shared/dist/chunk-MKT45LRA.mjs
9544
+ // ../shared/dist/chunk-SWQOZWHP.mjs
9545
+ var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
9546
+ var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
9547
+ function isSafeImageUrl(value) {
9548
+ if (value === null || value === void 0) return true;
9549
+ const trimmed = value.trim();
9550
+ if (trimmed === "") return true;
9551
+ if (!HAS_SCHEME_RE.test(trimmed)) return true;
9552
+ return SAFE_IMAGE_URL_RE.test(trimmed);
9553
+ }
9554
+
9555
+ // ../shared/dist/chunk-KUMPS6RH.mjs
9556
+ var SUPPORTED_APPILOTS_LOCALES = ["pt-BR", "en", "es", "fr"];
9557
+
9558
+ // ../shared/dist/chunk-2GTRKNPJ.mjs
9559
+ var symbols = external_exports.array(external_exports.string().max(240)).max(12);
9560
+ var controlEvidenceSchema = external_exports.object({
9561
+ version: external_exports.literal(1),
9562
+ siteId: external_exports.string().regex(/^[a-f0-9]{20}$/),
9563
+ component: external_exports.string().max(240),
9564
+ icons: symbols,
9565
+ handler: external_exports.string().max(240).optional(),
9566
+ calls: symbols,
9567
+ argumentBindings: symbols,
9568
+ conditions: symbols,
9569
+ nativeConfirmation: external_exports.object({
9570
+ title: external_exports.string().max(240).optional(),
9571
+ destructiveOption: external_exports.boolean()
9572
+ }).optional()
9573
+ });
8108
9574
  var locatorSourceSchema = external_exports.enum([
8109
9575
  "appilotsId",
8110
9576
  "testID",
@@ -8283,6 +9749,23 @@ var actionDescriptorSchema = external_exports.object({
8283
9749
  effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
8284
9750
  riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
8285
9751
  requiresConfirmation: external_exports.boolean().optional(),
9752
+ /**
9753
+ * How long this action's work is expected to take, in milliseconds,
9754
+ * DECLARED by the app — not inferred (that is `appilotsInferred`).
9755
+ *
9756
+ * The SDK's post-action wait was a constant: 6s, or 10s when the
9757
+ * generator could prove the handler awaits something. No constant
9758
+ * fits, because app operations run from ~100ms to minutes, and the
9759
+ * failure is silent in both directions — too short and the agent
9760
+ * photographs a loading screen with no controls on it, too long and
9761
+ * every fast press pays for the slowest one. Only the app knows.
9762
+ *
9763
+ * Absent means absent: the SDK keeps its current defaults, so a
9764
+ * document generated before this field existed behaves exactly as it
9765
+ * did. The SDK also clamps the value to its own safety ceiling — a
9766
+ * declared budget is a request, not a licence to hang the session.
9767
+ */
9768
+ asyncBudgetMs: external_exports.number().int().positive().optional(),
8286
9769
  appilotsInferred: appilotsInferredActionSchema.optional()
8287
9770
  }).passthrough();
8288
9771
  var screenPermissionDescriptorSchema = external_exports.object({
@@ -8465,7 +9948,7 @@ external_exports.object({
8465
9948
  version: external_exports.string().default("1.0"),
8466
9949
  content: external_exports.record(external_exports.unknown())
8467
9950
  });
8468
- var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
9951
+ var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
8469
9952
  var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
8470
9953
  external_exports.object({
8471
9954
  name: external_exports.string().min(1).max(100),
@@ -8476,8 +9959,8 @@ external_exports.object({
8476
9959
  scope: apiKeyScopeSchema.default("sdk"),
8477
9960
  environment: apiKeyEnvironmentSchema.optional(),
8478
9961
  expiresAt: external_exports.string().datetime().optional()
8479
- }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
8480
- message: 'projectId is required for scope "sdk"',
9962
+ }).refine((v) => v.scope === "operator" || !!v.projectId, {
9963
+ message: "projectId is required for SDK and publishing keys",
8481
9964
  path: ["projectId"]
8482
9965
  });
8483
9966
  var boundedString = (max) => external_exports.string().max(max);
@@ -8502,10 +9985,21 @@ var snapshotInputSchema = external_exports.object({
8502
9985
  type: boundedString(40).optional(),
8503
9986
  required: external_exports.boolean().optional(),
8504
9987
  invalid: external_exports.boolean().optional(),
9988
+ /**
9989
+ * Whether the field holds anything, as its own fact rather than an
9990
+ * inference over `value`. The value is the user's and is the first
9991
+ * thing a privacy policy withholds; the existence of a value is the
9992
+ * agent's and is what stops it filling the same field twice.
9993
+ */
9994
+ filled: external_exports.boolean().optional(),
9995
+ /** This input has keyboard focus right now. */
9996
+ focused: external_exports.boolean().optional(),
8505
9997
  inModal: external_exports.boolean().optional()
8506
9998
  }).passthrough();
8507
9999
  var snapshotButtonSchema = external_exports.object({
10000
+ controlEvidence: controlEvidenceSchema.optional(),
8508
10001
  id: boundedString(160).optional(),
10002
+ dispatchable: external_exports.boolean().optional(),
8509
10003
  provenance: identityProvenanceSchema.optional(),
8510
10004
  /**
8511
10005
  * False when the control is mounted but currently OUTSIDE the window —
@@ -8566,7 +10060,36 @@ var snapshotListSchema = external_exports.object({
8566
10060
  */
8567
10061
  source: boundedString(40).optional(),
8568
10062
  itemCount: external_exports.number().int().optional(),
10063
+ /**
10064
+ * Size of the whole collection when the app declared it, for a list
10065
+ * that is a WINDOW onto more data than it holds.
10066
+ *
10067
+ * `itemCount` is how many rows the list is rendering from and
10068
+ * `visibleItemCount` how many of those are mounted; neither can
10069
+ * express "there are 36 and you are looking at the first 20",
10070
+ * because a paginated list's `data` is the page. Absent means
10071
+ * unknown — never "same as itemCount".
10072
+ */
10073
+ totalItemCount: external_exports.number().int().optional(),
8569
10074
  visibleItemCount: external_exports.number().int().optional(),
10075
+ viewportItemCount: external_exports.number().int().nonnegative().optional(),
10076
+ exploration: external_exports.object({
10077
+ revision: external_exports.number().int().nonnegative(),
10078
+ observedItemCount: external_exports.number().int().min(0).max(5e3),
10079
+ observedRanges: external_exports.array(
10080
+ external_exports.object({
10081
+ start: external_exports.number().int().nonnegative(),
10082
+ end: external_exports.number().int().nonnegative()
10083
+ })
10084
+ ).max(32),
10085
+ rangesTruncated: external_exports.boolean(),
10086
+ coverage: external_exports.enum(["partial", "all-loaded"]),
10087
+ pagination: external_exports.enum(["possible", "not-declared"]),
10088
+ scrollSteps: external_exports.number().int().nonnegative(),
10089
+ remainingScrollSteps: external_exports.number().int().nonnegative(),
10090
+ consecutiveNoProgress: external_exports.number().int().nonnegative(),
10091
+ lastScroll: external_exports.enum(["moved", "no-progress", "boundary", "unverified"]).optional()
10092
+ }).optional(),
8570
10093
  refreshing: external_exports.boolean().optional(),
8571
10094
  empty: external_exports.boolean().optional(),
8572
10095
  label: boundedString(300).optional(),
@@ -8614,6 +10137,7 @@ var snapshotChoiceGroupSchema = external_exports.object({
8614
10137
  }).passthrough();
8615
10138
  var snapshotElementSchema = external_exports.object({
8616
10139
  id: boundedString(160).optional(),
10140
+ dispatchable: external_exports.boolean().optional(),
8617
10141
  role: boundedString(40).optional(),
8618
10142
  label: boundedString(300).optional(),
8619
10143
  texts: external_exports.array(boundedString(500)).max(50).optional(),
@@ -8657,6 +10181,34 @@ var agentSnapshotSchema = external_exports.object({
8657
10181
  * it. Optional: older SDKs never clamp and never send it.
8658
10182
  */
8659
10183
  truncated: external_exports.boolean().optional(),
10184
+ /**
10185
+ * What moved since the previous observation, computed on the device
10186
+ * because that is the only side holding both snapshots.
10187
+ *
10188
+ * Shape only — counts, booleans, and ids the app declared itself — so
10189
+ * it survives an observation whose content was withheld. ABSENT means
10190
+ * there was no previous observation to compare against; `unchanged:
10191
+ * true` means we compared and nothing moved, which is the strongest
10192
+ * evidence available that an action did nothing. The two must not be
10193
+ * collapsed, for the same reason `truncated` exists.
10194
+ */
10195
+ delta: external_exports.object({
10196
+ routeChanged: external_exports.boolean().optional(),
10197
+ modalOpened: external_exports.boolean().optional(),
10198
+ modalClosed: external_exports.boolean().optional(),
10199
+ loadingStarted: external_exports.boolean().optional(),
10200
+ loadingFinished: external_exports.boolean().optional(),
10201
+ textsAdded: external_exports.number().int().nonnegative().optional(),
10202
+ textsRemoved: external_exports.number().int().nonnegative().optional(),
10203
+ buttonsAddedIndices: external_exports.array(external_exports.number().int().nonnegative()).max(12).optional(),
10204
+ buttonsRemoved: external_exports.number().int().nonnegative().optional(),
10205
+ visibleRowsDelta: external_exports.number().int().optional(),
10206
+ totalRowsDelta: external_exports.number().int().optional(),
10207
+ fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
10208
+ fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
10209
+ invalidAppeared: external_exports.boolean().optional(),
10210
+ unchanged: external_exports.boolean().optional()
10211
+ }).passthrough().optional(),
8660
10212
  /**
8661
10213
  * How many of this screen's controls the client could name, split by
8662
10214
  * `identityProvenance`. Diagnostic only — the relay never grounds an
@@ -8675,6 +10227,10 @@ var agentSnapshotSchema = external_exports.object({
8675
10227
  }).passthrough().optional()
8676
10228
  }).passthrough();
8677
10229
  var agentContextSchema = external_exports.object({
10230
+ missionProtocol: external_exports.literal(1).optional(),
10231
+ missionId: boundedString(160).optional(),
10232
+ /** Preferred supported device language, reported by the SDK independently of map/UI labels. */
10233
+ deviceLocale: external_exports.enum(SUPPORTED_APPILOTS_LOCALES).optional(),
8678
10234
  /**
8679
10235
  * Client platform this observation was captured on. Optional and
8680
10236
  * additive (see `clientPlatformSchema`) — absent means
@@ -8690,7 +10246,19 @@ var agentContextSchema = external_exports.object({
8690
10246
  rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8691
10247
  currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8692
10248
  routeNames: external_exports.array(boundedString(200)).max(500).optional(),
8693
- canGoBack: external_exports.boolean().optional()
10249
+ canGoBack: external_exports.boolean().optional(),
10250
+ /**
10251
+ * The stack a back press pops through, oldest first, ending on
10252
+ * the current screen. `canGoBack` says a back exists; this says
10253
+ * where it goes.
10254
+ */
10255
+ backStack: external_exports.array(boundedString(200)).max(50).optional(),
10256
+ /**
10257
+ * Screens the session has been on, oldest first. Names only —
10258
+ * params carry record ids and often personal data, and a route
10259
+ * name is structure.
10260
+ */
10261
+ visited: external_exports.array(boundedString(200)).max(8).optional()
8694
10262
  }).passthrough().optional(),
8695
10263
  screenMetadata: external_exports.object({
8696
10264
  name: boundedString(200).optional(),
@@ -8796,7 +10364,7 @@ var formFillPayloadSchema = external_exports.object({
8796
10364
  submitAfterFill: external_exports.boolean().optional()
8797
10365
  }).passthrough();
8798
10366
  var uiInteractionPayloadSchema = external_exports.object({
8799
- action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
10367
+ action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
8800
10368
  // `targetId` is the canonical server/LLM field. SDK runtimes still
8801
10369
  // accept `componentId` as a compatibility alias.
8802
10370
  targetId: external_exports.string().min(1),
@@ -8913,6 +10481,12 @@ var actionDiagnoseSchema = external_exports.object({
8913
10481
  requiresUserInput: external_exports.boolean().optional()
8914
10482
  });
8915
10483
  var actionResultSchema = external_exports.object({
10484
+ nativeConfirmation: external_exports.object({
10485
+ title: external_exports.string().max(300),
10486
+ message: external_exports.string().max(1e3).optional(),
10487
+ buttonLabel: external_exports.string().max(160).optional(),
10488
+ handlerCompleted: external_exports.boolean()
10489
+ }).optional(),
8916
10490
  actionId: external_exports.string(),
8917
10491
  type: external_exports.string(),
8918
10492
  success: external_exports.boolean(),
@@ -8964,8 +10538,15 @@ external_exports.object({
8964
10538
  // issue #169 — the observation the server grounds targets against, now
8965
10539
  // validated + bounded at the border instead of z.record(z.unknown()).
8966
10540
  context: agentContextSchema.optional(),
8967
- /** Hop counter — server enforces a cap to prevent runaway loops */
8968
- hop: external_exports.number().int().min(1).max(10).optional()
10541
+ /**
10542
+ * Hop counter — server enforces a cap to prevent runaway loops.
10543
+ * The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
10544
+ * #503, apps/api/src/modules/agents/routes.ts): the first over-budget
10545
+ * hop has to get through validation so the route can answer it with
10546
+ * the friendly automation-limit message instead of a 422. The +3
10547
+ * headroom mirrors what 7/10 was before the recalibration.
10548
+ */
10549
+ hop: external_exports.number().int().min(1).max(13).optional()
8969
10550
  });
8970
10551
  var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
8971
10552
  var screenPermissionSchema = external_exports.object({
@@ -9033,7 +10614,7 @@ external_exports.object({
9033
10614
  /** null = remove webhook, undefined = leave unchanged. */
9034
10615
  budgetWebhookUrl: external_exports.string().url().nullable().optional()
9035
10616
  }).strict();
9036
- var localeSchema = external_exports.enum(["pt-BR", "en", "es"]);
10617
+ var localeSchema = external_exports.enum(SUPPORTED_APPILOTS_LOCALES);
9037
10618
  var hexColorSchema = external_exports.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
9038
10619
  message: "Must be a hex color like #6366f1"
9039
10620
  });
@@ -9071,15 +10652,18 @@ var themeTokensSchema = external_exports.object({
9071
10652
  */
9072
10653
  mode: external_exports.enum(["auto", "light", "dark"]).optional()
9073
10654
  }).strict();
10655
+ var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
10656
+ message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
10657
+ });
9074
10658
  external_exports.object({
9075
10659
  /** Tone / persona instructions appended to the system prompt. */
9076
10660
  personaPrompt: external_exports.string().max(2048).nullable(),
9077
10661
  /** Display name in the chat header (e.g. "Aria"). */
9078
10662
  assistantName: external_exports.string().max(60).nullable(),
9079
10663
  /** URL or remote asset id for the avatar shown next to assistant turns. */
9080
- assistantAvatar: external_exports.string().max(500).nullable(),
10664
+ assistantAvatar: imageSourceField.nullable(),
9081
10665
  /** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
9082
- emptyStateIcon: external_exports.string().max(500).nullable(),
10666
+ emptyStateIcon: imageSourceField.nullable(),
9083
10667
  /** First-message text shown in the empty state. */
9084
10668
  welcomeMessage: external_exports.string().max(500).nullable(),
9085
10669
  /** Title rendered at the top of the chat. */
@@ -9093,20 +10677,20 @@ external_exports.object({
9093
10677
  /** Chat-open FAB background color. Null falls back to theme.colors.primary. */
9094
10678
  triggerButtonColor: hexColorSchema.nullable(),
9095
10679
  /** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
9096
- triggerButtonImageUrl: external_exports.string().max(500).nullable()
10680
+ triggerButtonImageUrl: imageSourceField.nullable()
9097
10681
  });
9098
10682
  external_exports.object({
9099
10683
  personaPrompt: external_exports.string().max(2048).nullable().optional(),
9100
10684
  assistantName: external_exports.string().max(60).nullable().optional(),
9101
- assistantAvatar: external_exports.string().max(500).nullable().optional(),
9102
- emptyStateIcon: external_exports.string().max(500).nullable().optional(),
10685
+ assistantAvatar: imageSourceField.nullable().optional(),
10686
+ emptyStateIcon: imageSourceField.nullable().optional(),
9103
10687
  welcomeMessage: external_exports.string().max(500).nullable().optional(),
9104
10688
  chatTitle: external_exports.string().max(60).nullable().optional(),
9105
10689
  poweredByVisible: external_exports.boolean().optional(),
9106
10690
  theme: themeTokensSchema.nullable().optional(),
9107
10691
  defaultLocale: localeSchema.nullable().optional(),
9108
10692
  triggerButtonColor: hexColorSchema.nullable().optional(),
9109
- triggerButtonImageUrl: external_exports.string().max(500).nullable().optional()
10693
+ triggerButtonImageUrl: imageSourceField.nullable().optional()
9110
10694
  }).strict();
9111
10695
  var sandboxObservationSchema = external_exports.object({
9112
10696
  route: external_exports.string().max(200).optional(),
@@ -9126,7 +10710,19 @@ var sandboxObservationSchema = external_exports.object({
9126
10710
  type: external_exports.string().max(40).optional(),
9127
10711
  placeholder: external_exports.string().max(200).optional(),
9128
10712
  /** Inline validation state (SDK snapshot field). */
9129
- invalid: external_exports.boolean().optional()
10713
+ invalid: external_exports.boolean().optional(),
10714
+ /**
10715
+ * Whether the field holds anything, as its own fact rather than
10716
+ * an inference over `value`.
10717
+ *
10718
+ * Here because the eval posts through this schema: a fact the
10719
+ * SDK produces and this contract has no word for is a fact the
10720
+ * corpus can never grade the agent on. `invalid` and `required`
10721
+ * were already here; these two were the half that was missing.
10722
+ */
10723
+ filled: external_exports.boolean().optional(),
10724
+ /** This input has keyboard focus right now. */
10725
+ focused: external_exports.boolean().optional()
9130
10726
  })
9131
10727
  ).max(100).optional(),
9132
10728
  buttons: external_exports.array(
@@ -9605,7 +11201,7 @@ var manifestSchema = external_exports.object({
9605
11201
  navigation: navigationGraphSchema.partial().optional()
9606
11202
  }).passthrough();
9607
11203
  async function loadManifest(rootDir, manifestPath) {
9608
- const resolvedPath = path8__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
11204
+ const resolvedPath = path2__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
9609
11205
  let raw;
9610
11206
  try {
9611
11207
  raw = await readFile(resolvedPath, "utf-8");
@@ -9688,6 +11284,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
9688
11284
  };
9689
11285
  }
9690
11286
 
11287
+ // src/generators/checksum.ts
11288
+ function serializeForChecksum(document) {
11289
+ const { generatedAt: _generatedAt, ...content } = document;
11290
+ return JSON.stringify(content, null, 2);
11291
+ }
11292
+
9691
11293
  // src/pipeline/enrichment.ts
9692
11294
  function enrichScreenForAgent(screen) {
9693
11295
  const targets = mergeTargets([
@@ -9764,7 +11366,10 @@ function mergeTargets(targets) {
9764
11366
  for (const target of targets) {
9765
11367
  if (!target.id) continue;
9766
11368
  const existing = byId.get(target.id);
9767
- byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
11369
+ byId.set(
11370
+ target.id,
11371
+ existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
11372
+ );
9768
11373
  }
9769
11374
  return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
9770
11375
  }
@@ -9809,7 +11414,10 @@ function synthesizeFlows(screen, targets) {
9809
11414
  intent: "destructive_action",
9810
11415
  steps: [
9811
11416
  { type: "press", target: action.id, label: action.label },
9812
- { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
11417
+ {
11418
+ type: "confirm",
11419
+ description: "Wait for native or custom confirmation before continuing"
11420
+ },
9813
11421
  { type: "wait", description: describeWait(action) }
9814
11422
  ],
9815
11423
  waitPolicy: waitPolicyForAction(action),
@@ -9824,10 +11432,19 @@ function synthesizeFlows(screen, targets) {
9824
11432
  title: `Act on an item in ${collection.id}`,
9825
11433
  intent: "list_action",
9826
11434
  steps: [
9827
- { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
9828
- { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
11435
+ {
11436
+ type: "choose-list-item",
11437
+ target: collection.id,
11438
+ description: "Resolve the user reference to a visible or searchable row"
11439
+ },
11440
+ {
11441
+ type: "press",
11442
+ description: collection.rowAction?.description ?? "Open the row action"
11443
+ }
9829
11444
  ],
9830
- waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
11445
+ waitPolicy: {
11446
+ expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
11447
+ }
9831
11448
  });
9832
11449
  }
9833
11450
  }
@@ -9835,25 +11452,32 @@ function synthesizeFlows(screen, targets) {
9835
11452
  }
9836
11453
  function waitPolicyForAction(action) {
9837
11454
  const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
9838
- const signals = [action.successSignal, action.failureSignal].filter(Boolean);
11455
+ const signals = [action.successSignal, action.failureSignal].filter(
11456
+ Boolean
11457
+ );
11458
+ const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
9839
11459
  return {
9840
11460
  ...expectedOutcome ? { expectedOutcome } : {},
9841
11461
  ...signals && signals.length > 0 ? { signals } : {},
9842
- ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
11462
+ ...maxMs !== void 0 ? { maxMs } : {}
9843
11463
  };
9844
11464
  }
9845
11465
  function describeWait(action) {
9846
11466
  if (action.successSignal?.description) return action.successSignal.description;
9847
- if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
11467
+ if (action.successSignal?.type === "goBack")
11468
+ return "Wait for the app to return to the previous screen";
9848
11469
  if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
9849
- if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
11470
+ if (action.appilotsInferred?.expectedOutcome)
11471
+ return `Wait for ${action.appilotsInferred.expectedOutcome}`;
9850
11472
  return "Wait for the UI to settle";
9851
11473
  }
9852
11474
  function synthesizeAgentHints(screen, targets, flows) {
9853
11475
  const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
9854
11476
  const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
9855
11477
  const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
9856
- const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
11478
+ const firstAsyncAction = screen.actions.find(
11479
+ (action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
11480
+ );
9857
11481
  const hints = {
9858
11482
  primaryGoal: synthesizePrimaryGoal(screen),
9859
11483
  commonTasks,
@@ -9880,7 +11504,9 @@ function synthesizePrimaryGoal(screen) {
9880
11504
  }
9881
11505
  const fieldCount = uniqueFields.size;
9882
11506
  if (fieldCount > 0) {
9883
- const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
11507
+ const requiredCount = Array.from(uniqueFields.values()).filter(
11508
+ (field) => field.required
11509
+ ).length;
9884
11510
  const fieldLabel = fieldCount === 1 ? "field" : "fields";
9885
11511
  goals.push(
9886
11512
  requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
@@ -9894,7 +11520,9 @@ function synthesizePrimaryGoal(screen) {
9894
11520
  if (submitCount > 0) {
9895
11521
  goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
9896
11522
  }
9897
- const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
11523
+ const asyncCount = screen.actions.filter(
11524
+ (action) => action.appilotsInferred?.isAsyncTrigger
11525
+ ).length;
9898
11526
  if (asyncCount > 0) {
9899
11527
  goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
9900
11528
  }
@@ -9969,7 +11597,7 @@ var MCPGenerator = class _MCPGenerator {
9969
11597
  const agentReadyScreens = mergedScreens.map(
9970
11598
  (screen) => enrichScreenForAgent({
9971
11599
  ...screen,
9972
- filePath: screen.filePath ? path8__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
11600
+ filePath: screen.filePath ? path2__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
9973
11601
  })
9974
11602
  );
9975
11603
  const projectInfo = await this.getProjectInfo();
@@ -9993,19 +11621,33 @@ var MCPGenerator = class _MCPGenerator {
9993
11621
  }
9994
11622
  };
9995
11623
  const serialized = JSON.stringify(document, null, 2);
9996
- const checksum = this.calculateChecksum(serialized);
9997
- const filePath = path8__default.resolve(outputDir, `mcp-document.${this.options.format}`);
11624
+ const checksum = this.calculateChecksum(serializeForChecksum(document));
11625
+ const filePath = path2__default.resolve(outputDir, `mcp-document.${this.options.format}`);
9998
11626
  await writeFile(filePath, serialized, "utf-8");
9999
11627
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10000
- const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
11628
+ const controlFiles = analyzed.controlEvidenceFiles ?? {};
11629
+ await writeFile(
11630
+ path2__default.resolve(outputDir, "control-evidence.json"),
11631
+ JSON.stringify({ version: 1, files: controlFiles }, null, 2),
11632
+ "utf-8"
11633
+ );
11634
+ const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
10001
11635
  await writeFile(checksumFilePath, checksum, "utf-8");
10002
11636
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
11637
+ const evidenceCount = Object.values(controlFiles).reduce(
11638
+ (sum, entries) => sum + entries.length,
11639
+ 0
11640
+ );
11641
+ console.log(
11642
+ `[MCPGenerator] Source evidence: ${evidenceCount} icon controls across ${Object.keys(controlFiles).length} files (runtime binding required)`
11643
+ );
10003
11644
  console.log("[MCPGenerator] Generation complete!");
10004
11645
  return {
10005
11646
  document,
10006
11647
  filePath,
10007
11648
  format: this.options.format,
10008
- checksum
11649
+ checksum,
11650
+ ...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
10009
11651
  };
10010
11652
  }
10011
11653
  /**
@@ -10017,7 +11659,7 @@ var MCPGenerator = class _MCPGenerator {
10017
11659
  * after calling `generate()`.
10018
11660
  */
10019
11661
  static async readPreviousChecksum(outputDir) {
10020
- const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
11662
+ const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
10021
11663
  try {
10022
11664
  const content = await readFile(checksumFilePath, "utf-8");
10023
11665
  return content.trim() || null;
@@ -10036,7 +11678,7 @@ var MCPGenerator = class _MCPGenerator {
10036
11678
  */
10037
11679
  async getProjectInfo() {
10038
11680
  try {
10039
- const packageJsonPath = path8__default.resolve(this.analyzerConfig.rootDir, "package.json");
11681
+ const packageJsonPath = path2__default.resolve(this.analyzerConfig.rootDir, "package.json");
10040
11682
  const packageJsonContent = await readFile(packageJsonPath, "utf-8");
10041
11683
  const packageJson = JSON.parse(packageJsonContent);
10042
11684
  return {
@@ -10064,7 +11706,8 @@ var KNOWN_CONFIG_KEYS = [
10064
11706
  "navigationExclude",
10065
11707
  "platform",
10066
11708
  "manifestPath",
10067
- "eval"
11709
+ "eval",
11710
+ "knowledge"
10068
11711
  ];
10069
11712
  var KEY_ALIASES = {
10070
11713
  apiUrl: "serverUrl",
@@ -10119,12 +11762,12 @@ function getEnvOverrides(env = process.env) {
10119
11762
  return trimmed ? trimmed : void 0;
10120
11763
  };
10121
11764
  return {
10122
- apiKey: clean(env.APPILOTS_API_KEY),
11765
+ apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
10123
11766
  projectId: clean(env.APPILOTS_PROJECT_ID),
10124
11767
  serverUrl: clean(env.APPILOTS_SERVER_URL)
10125
11768
  };
10126
11769
  }
10127
- function loadConfig(onWarn) {
11770
+ function loadConfig(onWarn, options = {}) {
10128
11771
  const configPath = getConfigPath();
10129
11772
  const env = getEnvOverrides();
10130
11773
  let fileConfig = {};
@@ -10163,6 +11806,10 @@ function loadConfig(onWarn) {
10163
11806
  };
10164
11807
  const validation = validateConfig(merged);
10165
11808
  validation.warnings.unshift(...legacyWarnings);
11809
+ if (options.requireApiKey === false) {
11810
+ validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
11811
+ validation.valid = validation.errors.length === 0;
11812
+ }
10166
11813
  if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
10167
11814
  for (const key of Object.keys(fileConfig)) {
10168
11815
  if (KNOWN_CONFIG_KEYS.includes(key)) continue;
@@ -10208,7 +11855,8 @@ function saveConfig(dir, config) {
10208
11855
  navigationExclude: config.navigationExclude || existingConfig?.navigationExclude,
10209
11856
  platform: config.platform || existingConfig?.platform,
10210
11857
  manifestPath: config.manifestPath || existingConfig?.manifestPath,
10211
- eval: config.eval || existingConfig?.eval
11858
+ eval: config.eval || existingConfig?.eval,
11859
+ knowledge: config.knowledge || existingConfig?.knowledge
10212
11860
  };
10213
11861
  try {
10214
11862
  writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), "utf-8");
@@ -10301,6 +11949,19 @@ function validateConfig(config) {
10301
11949
  if (config.manifestPath !== void 0 && typeof config.manifestPath !== "string") {
10302
11950
  errors.push("manifestPath must be a string");
10303
11951
  }
11952
+ if (config.knowledge !== void 0) {
11953
+ if (typeof config.knowledge !== "object" || config.knowledge === null || Array.isArray(config.knowledge)) {
11954
+ errors.push("knowledge must be an object");
11955
+ } else {
11956
+ const kc = config.knowledge;
11957
+ for (const field of ["sources", "exclude"]) {
11958
+ const value = kc[field];
11959
+ if (value !== void 0 && (!Array.isArray(value) || !value.every((item) => typeof item === "string"))) {
11960
+ errors.push(`knowledge.${field} must be an array of strings`);
11961
+ }
11962
+ }
11963
+ }
11964
+ }
10304
11965
  if (config.eval !== void 0) {
10305
11966
  if (typeof config.eval !== "object" || config.eval === null || Array.isArray(config.eval)) {
10306
11967
  errors.push("eval must be an object");
@@ -10577,6 +12238,48 @@ var AppilotsAPIClient = class {
10577
12238
  };
10578
12239
  }
10579
12240
  }
12241
+ /**
12242
+ * Syncs knowledge documents with the Appilots backend.
12243
+ *
12244
+ * @param documents Array of documents to sync (filename, base64 content, mimeType, checksum)
12245
+ * @returns KnowledgeSyncResult with counts of uploaded, skipped, and errored docs
12246
+ */
12247
+ async knowledgeSync(documents) {
12248
+ try {
12249
+ const response = await this.request(`${this.baseUrl}/cli/knowledge/sync`, {
12250
+ method: "POST",
12251
+ headers: {
12252
+ "Content-Type": "application/json",
12253
+ Authorization: `Bearer ${this.apiKey}`
12254
+ },
12255
+ body: JSON.stringify({ documents })
12256
+ });
12257
+ if (!response.ok) {
12258
+ const errorData = await response.json().catch(() => ({}));
12259
+ return {
12260
+ success: false,
12261
+ uploaded: 0,
12262
+ skipped: 0,
12263
+ errors: 0,
12264
+ error: describeApiError(errorData, `HTTP ${response.status}: ${response.statusText}`)
12265
+ };
12266
+ }
12267
+ const json = await response.json();
12268
+ const inner = json.data ?? json;
12269
+ return {
12270
+ success: true,
12271
+ ...inner
12272
+ };
12273
+ } catch (error) {
12274
+ return {
12275
+ success: false,
12276
+ uploaded: 0,
12277
+ skipped: 0,
12278
+ errors: 0,
12279
+ error: error instanceof Error ? error.message : "Failed to sync knowledge with Appilots API"
12280
+ };
12281
+ }
12282
+ }
10580
12283
  /**
10581
12284
  * Checks if the Appilots API server is healthy
10582
12285
  *
@@ -10595,7 +12298,7 @@ var AppilotsAPIClient = class {
10595
12298
  };
10596
12299
 
10597
12300
  // src/version.ts
10598
- var CLI_VERSION = "0.11.0";
12301
+ var CLI_VERSION = "0.13.0";
10599
12302
 
10600
12303
  export { AppilotsAPIClient, CLI_VERSION, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
10601
12304
  //# sourceMappingURL=index.mjs.map