@appilots/cli 0.10.0 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  var fs = require('fs/promises');
4
- var path8 = require('path');
4
+ var path2 = require('path');
5
5
  var traverse4 = require('@babel/traverse');
6
6
  var BabelTypes = require('@babel/types');
7
- var glob = require('fast-glob');
7
+ var fastGlob = require('fast-glob');
8
8
  var parser = require('@babel/parser');
9
9
  var fs$1 = require('fs');
10
10
  var crypto = require('crypto');
@@ -30,10 +30,10 @@ function _interopNamespace(e) {
30
30
  }
31
31
 
32
32
  var fs__default = /*#__PURE__*/_interopDefault(fs);
33
- var path8__namespace = /*#__PURE__*/_interopNamespace(path8);
33
+ var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
34
34
  var traverse4__default = /*#__PURE__*/_interopDefault(traverse4);
35
35
  var BabelTypes__namespace = /*#__PURE__*/_interopNamespace(BabelTypes);
36
- var glob__default = /*#__PURE__*/_interopDefault(glob);
36
+ var fastGlob__default = /*#__PURE__*/_interopDefault(fastGlob);
37
37
  var parser__namespace = /*#__PURE__*/_interopNamespace(parser);
38
38
 
39
39
  var __defProp = Object.defineProperty;
@@ -41,6 +41,13 @@ var __export = (target, all) => {
41
41
  for (var name in all)
42
42
  __defProp(target, name, { get: all[name], enumerable: true });
43
43
  };
44
+ function byCodeUnit(a, b) {
45
+ return a < b ? -1 : a > b ? 1 : 0;
46
+ }
47
+ async function globSorted(patterns, options) {
48
+ const files = await fastGlob__default.default(patterns, options);
49
+ return files.sort(byCodeUnit);
50
+ }
44
51
  var DEFAULT_PARSER_PLUGINS = [
45
52
  "jsx",
46
53
  "typescript",
@@ -425,14 +432,25 @@ var ScreenAnalyzer = class {
425
432
  strictScreens;
426
433
  /** Glob patterns that identify screen files in strict mode */
427
434
  screenPatterns;
435
+ /**
436
+ * Arquivos que uma ROTA monta (`component={…}` resolvido pelo
437
+ * `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
438
+ * convenção de nome, porque um componente que uma rota monta É uma tela por
439
+ * definição — não é heurística, é o que o app declarou.
440
+ *
441
+ * É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
442
+ * em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
443
+ */
444
+ routeTargetFiles;
428
445
  /** §D: Count of screens filtered out in strict mode (available after analyze()) */
429
446
  screensFilteredOut = 0;
430
447
  constructor(config, options) {
431
448
  this.config = config;
449
+ this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
432
450
  this.strictScreens = options?.strictScreens ?? false;
433
451
  this.screenPatterns = options?.screenPatterns ?? [
434
- "**/*Screen.{ts,tsx}",
435
- "**/screens/**/*.{ts,tsx}"
452
+ "**/*Screen.{ts,tsx,js,jsx}",
453
+ "**/screens/**/*.{ts,tsx,js,jsx}"
436
454
  ];
437
455
  }
438
456
  /** Analyze all screens in the project */
@@ -444,7 +462,7 @@ var ScreenAnalyzer = class {
444
462
  console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
445
463
  }
446
464
  }
447
- const files = await glob__default.default(include, {
465
+ const files = await globSorted(include, {
448
466
  cwd: this.config.rootDir,
449
467
  ignore: exclude
450
468
  });
@@ -453,23 +471,24 @@ var ScreenAnalyzer = class {
453
471
  }
454
472
  let screenPatternFiles = null;
455
473
  if (this.strictScreens) {
456
- const matched = await glob__default.default(this.screenPatterns, {
474
+ const matched = await globSorted(this.screenPatterns, {
457
475
  cwd: this.config.rootDir,
458
476
  ignore: exclude
459
477
  });
460
- screenPatternFiles = new Set(matched.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
478
+ screenPatternFiles = new Set(matched.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
461
479
  }
462
480
  const screens = [];
463
481
  this.screensFilteredOut = 0;
464
482
  for (const file of files) {
465
- const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
483
+ const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
466
484
  try {
467
485
  const descriptor = await this.analyzeFile(filePath);
468
486
  if (!descriptor) continue;
469
487
  if (this.strictScreens) {
470
488
  const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
471
489
  const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
472
- if (!hasRegisterScreen && !matchesPattern) {
490
+ const isRouteTarget = this.routeTargetFiles.has(filePath);
491
+ if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
473
492
  this.screensFilteredOut++;
474
493
  if (this.verbose) {
475
494
  console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
@@ -677,6 +696,8 @@ var ScreenAnalyzer = class {
677
696
  action.riskLevel = value.value;
678
697
  } else if (key === "nativeConfirmationExpected" && BabelTypes__namespace.isBooleanLiteral(value)) {
679
698
  action.nativeConfirmationExpected = value.value;
699
+ } else if (key === "asyncBudgetMs" && BabelTypes__namespace.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
700
+ action.asyncBudgetMs = value.value;
680
701
  } else if (key === "appilotsInferred" && BabelTypes__namespace.isObjectExpression(value)) {
681
702
  action.appilotsInferred = this.parseAppilotsInferredObject(value);
682
703
  }
@@ -1656,43 +1677,871 @@ var ScreenAnalyzer = class {
1656
1677
  * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1657
1678
  */
1658
1679
  extractScreenName(filePath) {
1659
- const basename2 = path8__namespace.default.basename(filePath);
1680
+ const basename2 = path2__namespace.default.basename(filePath);
1660
1681
  return basename2.replace(/\.(tsx?|jsx?)$/, "");
1661
1682
  }
1662
1683
  };
1684
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
1685
+ var MAX_HOPS = 8;
1686
+ var ModuleGraph = class {
1687
+ asts = /* @__PURE__ */ new Map();
1688
+ resolved = /* @__PURE__ */ new Map();
1689
+ /** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
1690
+ aliases;
1691
+ /** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
1692
+ workspacePackages;
1693
+ constructor(rootDir) {
1694
+ this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
1695
+ this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
1696
+ }
1697
+ /** AST de um arquivo, memoizada. `null` quando não parseia. */
1698
+ parse(file) {
1699
+ const cached = this.asts.get(file);
1700
+ if (cached !== void 0) return cached;
1701
+ let ast = null;
1702
+ try {
1703
+ ast = parser__namespace.parse(fs$1.readFileSync(file, "utf-8"), {
1704
+ sourceType: "module",
1705
+ plugins: ["jsx", "typescript"]
1706
+ });
1707
+ } catch {
1708
+ ast = null;
1709
+ }
1710
+ this.asts.set(file, ast);
1711
+ return ast;
1712
+ }
1713
+ /**
1714
+ * `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
1715
+ * Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
1716
+ * é de terceiro e não tem tela nossa dentro.
1717
+ */
1718
+ resolve(fromFile, spec) {
1719
+ const key = `${fromFile} ${spec}`;
1720
+ const cached = this.resolved.get(key);
1721
+ if (cached !== void 0) return cached;
1722
+ let base = null;
1723
+ if (spec.startsWith(".")) {
1724
+ base = path2__namespace.default.resolve(path2__namespace.default.dirname(fromFile), spec);
1725
+ } else {
1726
+ for (const { prefix, target } of this.aliases) {
1727
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
1728
+ base = path2__namespace.default.join(target, spec.slice(prefix.length));
1729
+ break;
1730
+ }
1731
+ }
1732
+ if (!base) {
1733
+ for (const pkg of this.workspacePackages) {
1734
+ if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
1735
+ base = path2__namespace.default.join(pkg.dir, spec.slice(pkg.name.length));
1736
+ break;
1737
+ }
1738
+ }
1739
+ }
1740
+ }
1741
+ if (!base) {
1742
+ this.resolved.set(key, null);
1743
+ return null;
1744
+ }
1745
+ const candidates = [
1746
+ base,
1747
+ ...EXTENSIONS.map((e) => base + e),
1748
+ ...EXTENSIONS.map((e) => path2__namespace.default.join(base, "index" + e))
1749
+ ];
1750
+ let found = null;
1751
+ for (const c of candidates) {
1752
+ try {
1753
+ if (fs$1.existsSync(c) && fs$1.statSync(c).isFile()) {
1754
+ found = c;
1755
+ break;
1756
+ }
1757
+ } catch {
1758
+ }
1759
+ }
1760
+ this.resolved.set(key, found);
1761
+ return found;
1762
+ }
1763
+ /** `import`s do arquivo, por nome local. */
1764
+ imports(file) {
1765
+ const out = /* @__PURE__ */ new Map();
1766
+ const ast = this.parse(file);
1767
+ if (!ast) return out;
1768
+ for (const stmt of ast.program.body) {
1769
+ if (!BabelTypes__namespace.isImportDeclaration(stmt)) continue;
1770
+ const source = stmt.source.value;
1771
+ for (const spec of stmt.specifiers) {
1772
+ if (BabelTypes__namespace.isImportDefaultSpecifier(spec)) {
1773
+ out.set(spec.local.name, { source, imported: "default" });
1774
+ } else if (BabelTypes__namespace.isImportNamespaceSpecifier(spec)) {
1775
+ out.set(spec.local.name, { source, imported: "*" });
1776
+ } else if (BabelTypes__namespace.isImportSpecifier(spec)) {
1777
+ const imported = BabelTypes__namespace.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
1778
+ out.set(spec.local.name, { source, imported });
1779
+ }
1780
+ }
1781
+ }
1782
+ return out;
1783
+ }
1784
+ /** `const X = <init>` no topo do arquivo, incluindo `export const`. */
1785
+ topLevelInit(file, name) {
1786
+ const ast = this.parse(file);
1787
+ if (!ast) return null;
1788
+ for (const stmt of ast.program.body) {
1789
+ const decl = BabelTypes__namespace.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
1790
+ if (!BabelTypes__namespace.isVariableDeclaration(decl)) continue;
1791
+ for (const d of decl.declarations) {
1792
+ if (BabelTypes__namespace.isIdentifier(d.id) && d.id.name === name && d.init) {
1793
+ return BabelTypes__namespace.isTSAsExpression(d.init) ? d.init.expression : d.init;
1794
+ }
1795
+ }
1796
+ }
1797
+ return null;
1798
+ }
1799
+ /**
1800
+ * Onde `name` é DEFINIDO — segue import e reexport de barrel.
1801
+ * Devolve o arquivo e o nome sob o qual ele é definido lá.
1802
+ */
1803
+ resolveBinding(file, name, hops = 0) {
1804
+ if (hops > MAX_HOPS) return null;
1805
+ if (this.topLevelInit(file, name) !== null) return { file, name };
1806
+ const binding = this.imports(file).get(name);
1807
+ if (binding) {
1808
+ const target = this.resolve(file, binding.source);
1809
+ if (!target) return null;
1810
+ const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
1811
+ const deeper = this.resolveBinding(target, next, hops + 1);
1812
+ return deeper ?? { file: target, name: next };
1813
+ }
1814
+ const ast = this.parse(file);
1815
+ if (ast) {
1816
+ for (const stmt of ast.program.body) {
1817
+ if (!BabelTypes__namespace.isExportNamedDeclaration(stmt) || !stmt.source) continue;
1818
+ for (const spec of stmt.specifiers) {
1819
+ if (!BabelTypes__namespace.isExportSpecifier(spec)) continue;
1820
+ const exported = BabelTypes__namespace.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
1821
+ if (exported !== name) continue;
1822
+ const target = this.resolve(file, stmt.source.value);
1823
+ if (!target) return null;
1824
+ const local = spec.local.name;
1825
+ return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
1826
+ }
1827
+ }
1828
+ }
1829
+ if (ast) {
1830
+ for (const stmt of ast.program.body) {
1831
+ if (!BabelTypes__namespace.isExportAllDeclaration(stmt)) continue;
1832
+ const target = this.resolve(file, stmt.source.value);
1833
+ if (!target || target === file) continue;
1834
+ const deeper = this.resolveBinding(target, name, hops + 1);
1835
+ if (deeper) return deeper;
1836
+ }
1837
+ }
1838
+ if (ast) {
1839
+ for (const stmt of ast.program.body) {
1840
+ if (!BabelTypes__namespace.isExportDefaultDeclaration(stmt)) continue;
1841
+ if (BabelTypes__namespace.isIdentifier(stmt.declaration)) {
1842
+ const local = stmt.declaration.name;
1843
+ if (local === name) return null;
1844
+ return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
1845
+ }
1846
+ }
1847
+ }
1848
+ return null;
1849
+ }
1850
+ /**
1851
+ * O valor string de uma expressão de nome de rota, ou `null`.
1852
+ *
1853
+ * Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
1854
+ * seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
1855
+ * com interpolação nem valor calculado, de propósito.
1856
+ */
1857
+ stringConstant(file, node, hops = 0) {
1858
+ if (!node || hops > MAX_HOPS) return null;
1859
+ if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
1860
+ if (BabelTypes__namespace.isTemplateLiteral(node)) {
1861
+ return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
1862
+ }
1863
+ if (BabelTypes__namespace.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
1864
+ const chain = memberChain(node);
1865
+ if (!chain) return null;
1866
+ const origin = this.resolveBinding(file, chain.root);
1867
+ if (!origin) return null;
1868
+ let current = this.topLevelInit(origin.file, origin.name);
1869
+ if (!current) return null;
1870
+ for (const key of chain.path) {
1871
+ if (!BabelTypes__namespace.isObjectExpression(current)) return null;
1872
+ const prop = objectProperty(current, key);
1873
+ if (!prop) return null;
1874
+ current = BabelTypes__namespace.isTSAsExpression(prop) ? prop.expression : prop;
1875
+ }
1876
+ return BabelTypes__namespace.isStringLiteral(current) ? current.value : null;
1877
+ }
1878
+ /**
1879
+ * O ARQUIVO onde vive o componente de uma rota, ou `null`.
1880
+ *
1881
+ * Aceita as três formas que o corpus mostrou: identificador
1882
+ * (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
1883
+ * e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
1884
+ * que é como o `pocketpal` monta todas as telas do Drawer).
1885
+ */
1886
+ componentFile(file, node, hops = 0) {
1887
+ if (!node || hops > MAX_HOPS) return null;
1888
+ if (BabelTypes__namespace.isCallExpression(node)) {
1889
+ for (const arg of node.arguments) {
1890
+ if (BabelTypes__namespace.isIdentifier(arg) || BabelTypes__namespace.isMemberExpression(arg)) {
1891
+ const inner = this.componentFile(file, arg, hops + 1);
1892
+ if (inner) return inner;
1893
+ }
1894
+ }
1895
+ return null;
1896
+ }
1897
+ if (BabelTypes__namespace.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
1898
+ const chain = memberChain(node);
1899
+ if (!chain) return null;
1900
+ const origin = this.resolveBinding(file, chain.root);
1901
+ if (!origin) return null;
1902
+ if (chain.path.length === 0) return origin.file;
1903
+ const init = this.topLevelInit(origin.file, origin.name);
1904
+ if (init && BabelTypes__namespace.isObjectExpression(init)) {
1905
+ const prop = objectProperty(init, chain.path[0]);
1906
+ if (prop) return this.componentFile(origin.file, prop, hops + 1);
1907
+ }
1908
+ const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
1909
+ return viaExport?.file ?? null;
1910
+ }
1911
+ /**
1912
+ * Os arquivos DO PROJETO que este arquivo renderiza como JSX.
1913
+ *
1914
+ * `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
1915
+ * Import de pacote devolve `null` no `resolve` e fica de fora: componente de
1916
+ * terceiro não tem tela nossa dentro.
1917
+ *
1918
+ * Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
1919
+ * extractor, e um navegador não é conteúdo de tela.
1920
+ */
1921
+ renderedComponentFiles(file) {
1922
+ const ast = this.parse(file);
1923
+ if (!ast) return [];
1924
+ const names = /* @__PURE__ */ new Set();
1925
+ const visit = (node) => {
1926
+ if (!node || typeof node !== "object") return;
1927
+ if (BabelTypes__namespace.isJSXOpeningElement(node) && BabelTypes__namespace.isJSXIdentifier(node.name)) {
1928
+ const n = node.name.name;
1929
+ if (/^[A-Z]/.test(n)) names.add(n);
1930
+ }
1931
+ for (const key of Object.keys(node)) {
1932
+ const value = node[key];
1933
+ if (Array.isArray(value)) {
1934
+ for (const item of value) visit(item);
1935
+ } else if (value && typeof value === "object" && "type" in value) {
1936
+ visit(value);
1937
+ }
1938
+ }
1939
+ };
1940
+ visit(ast.program);
1941
+ const out = /* @__PURE__ */ new Set();
1942
+ for (const name of names) {
1943
+ const origin = this.resolveBinding(file, name);
1944
+ if (origin && origin.file !== file) out.add(origin.file);
1945
+ }
1946
+ return [...out];
1947
+ }
1948
+ };
1949
+ function memberChain(node) {
1950
+ const chain = [];
1951
+ let current = node;
1952
+ while (BabelTypes__namespace.isMemberExpression(current)) {
1953
+ if (current.computed) {
1954
+ if (!BabelTypes__namespace.isStringLiteral(current.property)) return null;
1955
+ chain.unshift(current.property.value);
1956
+ } else if (BabelTypes__namespace.isIdentifier(current.property)) {
1957
+ chain.unshift(current.property.name);
1958
+ } else {
1959
+ return null;
1960
+ }
1961
+ current = current.object;
1962
+ }
1963
+ return BabelTypes__namespace.isIdentifier(current) ? { root: current.name, path: chain } : null;
1964
+ }
1965
+ function objectProperty(obj, key) {
1966
+ for (const prop of obj.properties) {
1967
+ if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
1968
+ const name = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
1969
+ if (name === key && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
1970
+ }
1971
+ return null;
1972
+ }
1973
+ function readTsconfigAliases(rootDir) {
1974
+ const file = path2__namespace.default.join(rootDir, "tsconfig.json");
1975
+ if (!fs$1.existsSync(file)) return [];
1976
+ let parsed;
1977
+ try {
1978
+ const raw = stripJsonComments(fs$1.readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
1979
+ parsed = JSON.parse(raw);
1980
+ } catch {
1981
+ return [];
1982
+ }
1983
+ const paths = parsed.compilerOptions?.paths;
1984
+ if (!paths) return [];
1985
+ const baseUrl = path2__namespace.default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
1986
+ const out = [];
1987
+ for (const [pattern, targets] of Object.entries(paths)) {
1988
+ const first = targets[0];
1989
+ if (typeof first !== "string") continue;
1990
+ out.push({
1991
+ prefix: pattern.replace(/\/?\*$/, ""),
1992
+ target: path2__namespace.default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
1993
+ });
1994
+ }
1995
+ return out.sort((a, b) => b.prefix.length - a.prefix.length);
1996
+ }
1997
+ function stripJsonComments(text) {
1998
+ let out = "";
1999
+ let inString = false;
2000
+ let escaped = false;
2001
+ for (let i = 0; i < text.length; i++) {
2002
+ const c = text[i];
2003
+ if (inString) {
2004
+ out += c;
2005
+ if (escaped) escaped = false;
2006
+ else if (c === "\\") escaped = true;
2007
+ else if (c === '"') inString = false;
2008
+ continue;
2009
+ }
2010
+ if (c === '"') {
2011
+ inString = true;
2012
+ out += c;
2013
+ continue;
2014
+ }
2015
+ if (c === "/" && text[i + 1] === "/") {
2016
+ while (i < text.length && text[i] !== "\n") i++;
2017
+ out += "\n";
2018
+ continue;
2019
+ }
2020
+ if (c === "/" && text[i + 1] === "*") {
2021
+ i += 2;
2022
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
2023
+ i++;
2024
+ continue;
2025
+ }
2026
+ out += c;
2027
+ }
2028
+ return out;
2029
+ }
2030
+ function readWorkspacePackages(rootDir) {
2031
+ const start = path2__namespace.default.resolve(rootDir);
2032
+ let dir = start;
2033
+ for (let i = 0; i < 6; i++) {
2034
+ const globs = workspaceGlobs(dir);
2035
+ if (globs.length > 0) {
2036
+ const packages = expandWorkspaceGlobs(dir, globs);
2037
+ const containsApp = packages.some(
2038
+ (p) => start === p.dir || start.startsWith(p.dir + path2__namespace.default.sep)
2039
+ );
2040
+ return containsApp ? packages : [];
2041
+ }
2042
+ const parent = path2__namespace.default.dirname(dir);
2043
+ if (parent === dir) break;
2044
+ dir = parent;
2045
+ }
2046
+ return [];
2047
+ }
2048
+ function workspaceGlobs(dir) {
2049
+ const pnpm = path2__namespace.default.join(dir, "pnpm-workspace.yaml");
2050
+ if (fs$1.existsSync(pnpm)) {
2051
+ try {
2052
+ const lines = fs$1.readFileSync(pnpm, "utf-8").split(/\r?\n/);
2053
+ const out = [];
2054
+ let inPackages = false;
2055
+ for (const line of lines) {
2056
+ if (/^packages:/.test(line)) {
2057
+ inPackages = true;
2058
+ continue;
2059
+ }
2060
+ if (inPackages) {
2061
+ const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
2062
+ if (m) out.push(m[1].trim());
2063
+ else if (/^\S/.test(line)) break;
2064
+ }
2065
+ }
2066
+ if (out.length) return out;
2067
+ } catch {
2068
+ }
2069
+ }
2070
+ const pkgPath = path2__namespace.default.join(dir, "package.json");
2071
+ if (!fs$1.existsSync(pkgPath)) return [];
2072
+ try {
2073
+ const pkg = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8"));
2074
+ const ws = pkg.workspaces;
2075
+ if (Array.isArray(ws)) return ws;
2076
+ if (ws && Array.isArray(ws.packages)) return ws.packages;
2077
+ } catch {
2078
+ }
2079
+ return [];
2080
+ }
2081
+ function expandWorkspaceGlobs(root, globs) {
2082
+ const out = [];
2083
+ const add = (dir) => {
2084
+ const pkgPath = path2__namespace.default.join(dir, "package.json");
2085
+ if (!fs$1.existsSync(pkgPath)) return;
2086
+ try {
2087
+ const name = JSON.parse(fs$1.readFileSync(pkgPath, "utf-8")).name;
2088
+ if (name) out.push({ name, dir });
2089
+ } catch {
2090
+ }
2091
+ };
2092
+ for (const glob of globs) {
2093
+ if (glob.endsWith("/*")) {
2094
+ const parent = path2__namespace.default.join(root, glob.slice(0, -2));
2095
+ let entries = [];
2096
+ try {
2097
+ entries = fs$1.readdirSync(parent);
2098
+ } catch {
2099
+ continue;
2100
+ }
2101
+ for (const entry of entries) {
2102
+ const dir = path2__namespace.default.join(parent, entry);
2103
+ try {
2104
+ if (fs$1.statSync(dir).isDirectory()) add(dir);
2105
+ } catch {
2106
+ }
2107
+ }
2108
+ } else if (!glob.includes("*")) {
2109
+ add(path2__namespace.default.join(root, glob));
2110
+ }
2111
+ }
2112
+ return out.sort((a, b) => b.name.length - a.name.length);
2113
+ }
2114
+ var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
2115
+ var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
2116
+ var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
2117
+ function baseName(file) {
2118
+ let name = file;
2119
+ for (const ext of ROUTE_EXTENSIONS) {
2120
+ if (name.endsWith(ext)) {
2121
+ name = name.slice(0, -ext.length);
2122
+ break;
2123
+ }
2124
+ }
2125
+ for (const suffix of PLATFORM_SUFFIXES) {
2126
+ if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
2127
+ }
2128
+ return name;
2129
+ }
2130
+ function isRouteFile(file) {
2131
+ return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
2132
+ }
2133
+ function isSpecial(base) {
2134
+ if (base === "_layout") return true;
2135
+ if (base.startsWith("+")) return base !== "+not-found";
2136
+ return base.startsWith("_");
2137
+ }
2138
+ function layoutType(file) {
2139
+ try {
2140
+ const src = fs$1.readFileSync(file, "utf-8");
2141
+ if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
2142
+ if (/<Drawer\b/.test(src)) return "drawer";
2143
+ } catch {
2144
+ }
2145
+ return "stack";
2146
+ }
2147
+ function findLayout(dir) {
2148
+ for (const ext of ROUTE_EXTENSIONS) {
2149
+ const candidate = path2__namespace.default.join(dir, "_layout" + ext);
2150
+ if (fs$1.existsSync(candidate)) return candidate;
2151
+ }
2152
+ return null;
2153
+ }
2154
+ function analyzeExpoRouter(appRoot) {
2155
+ let routeDir = null;
2156
+ for (const candidate of ROUTE_DIR_CANDIDATES) {
2157
+ const full = path2__namespace.default.join(appRoot, candidate);
2158
+ if (fs$1.existsSync(full) && findLayout(full)) {
2159
+ routeDir = full;
2160
+ break;
2161
+ }
2162
+ }
2163
+ if (!routeDir) return null;
2164
+ const routes = [];
2165
+ const navigators = /* @__PURE__ */ new Map();
2166
+ const seen = /* @__PURE__ */ new Set();
2167
+ const walk = (dir, segments, navigator) => {
2168
+ const layout = findLayout(dir);
2169
+ const current = layout ? segments.join("/") || "/" : navigator;
2170
+ if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
2171
+ let entries;
2172
+ try {
2173
+ entries = fs$1.readdirSync(dir);
2174
+ } catch {
2175
+ return;
2176
+ }
2177
+ for (const entry of entries.sort()) {
2178
+ const full = path2__namespace.default.join(dir, entry);
2179
+ let isDir = false;
2180
+ try {
2181
+ isDir = fs$1.statSync(full).isDirectory();
2182
+ } catch {
2183
+ continue;
2184
+ }
2185
+ if (isDir) {
2186
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
2187
+ walk(full, [...segments, entry], current);
2188
+ continue;
2189
+ }
2190
+ if (!isRouteFile(entry)) continue;
2191
+ const base = baseName(entry);
2192
+ if (isSpecial(base)) continue;
2193
+ const routeSegments = base === "index" ? segments : [...segments, base];
2194
+ const name = "/" + routeSegments.join("/");
2195
+ if (seen.has(name)) continue;
2196
+ seen.add(name);
2197
+ routes.push({
2198
+ name,
2199
+ componentFile: full,
2200
+ navigatorName: current,
2201
+ navigatorType: navigators.get(current) ?? "stack",
2202
+ params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
2203
+ });
2204
+ }
2205
+ };
2206
+ walk(routeDir, [], "/");
2207
+ return {
2208
+ routeDir: path2__namespace.default.relative(appRoot, routeDir),
2209
+ routes,
2210
+ navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
2211
+ };
2212
+ }
2213
+
2214
+ // src/analyzers/NavigationAnalyzer.ts
2215
+ var NAVIGATOR_FACTORIES = {
2216
+ createStackNavigator: "stack",
2217
+ createNativeStackNavigator: "stack",
2218
+ createBottomTabNavigator: "tab",
2219
+ createTabNavigator: "tab",
2220
+ createMaterialTopTabNavigator: "tab",
2221
+ createMaterialBottomTabNavigator: "tab",
2222
+ createDrawerNavigator: "drawer"
2223
+ };
2224
+ var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
2225
+ function unwrapStaticScreen(value) {
2226
+ if (BabelTypes__namespace.isCallExpression(value)) {
2227
+ const arg = value.arguments[0];
2228
+ return arg ? unwrapStaticScreen(arg) : null;
2229
+ }
2230
+ if (BabelTypes__namespace.isObjectExpression(value)) {
2231
+ for (const prop of value.properties) {
2232
+ if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
2233
+ const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : null;
2234
+ if (key === "screen" && BabelTypes__namespace.isExpression(prop.value)) return prop.value;
2235
+ }
2236
+ return null;
2237
+ }
2238
+ return BabelTypes__namespace.isExpression(value) ? value : null;
2239
+ }
2240
+ function inferNavigatorType(factoryName) {
2241
+ if (/drawer/i.test(factoryName)) return "drawer";
2242
+ if (/tab/i.test(factoryName)) return "tab";
2243
+ return "stack";
2244
+ }
2245
+ var NAVIGATION_EVIDENCE = new RegExp(
2246
+ // `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
2247
+ // não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
2248
+ // `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
2249
+ // que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
2250
+ // junção entre arquivos nunca chega a ser tentada.
2251
+ // Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
2252
+ // fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
2253
+ // seis navegadores do `bluesky`.
2254
+ `create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
2255
+ );
1663
2256
  var NavigationAnalyzer = class {
1664
2257
  config;
1665
2258
  /** Extra glob patterns for navigation file discovery (added to defaults) */
1666
2259
  navigationInclude;
1667
2260
  /** Extra glob patterns to exclude from navigation analysis */
1668
2261
  navigationExclude;
2262
+ /** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
2263
+ graph;
2264
+ /**
2265
+ * Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
2266
+ * de navegação e a árvore de telas; sem ela as duas metades do documento
2267
+ * falam de coisas diferentes. Populada por `analyze()`.
2268
+ */
2269
+ routeTargets = /* @__PURE__ */ new Map();
2270
+ /**
2271
+ * Arquivos que DECLARAM uma fábrica de navegador.
2272
+ *
2273
+ * Um navegador aninhado é montado como `component=` de uma rota — no
2274
+ * `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
2275
+ * Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
2276
+ * exatamente o falso positivo que o corpus existe para não repetir. Um
2277
+ * navegador é um contêiner de rotas; a tela está um nível abaixo.
2278
+ *
2279
+ * Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
2280
+ * RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
2281
+ * `DetailViewStack` num arquivo e escreve o JSX em
2282
+ * `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
2283
+ * segundo entrar como tela.
2284
+ */
2285
+ navigatorFiles = /* @__PURE__ */ new Set();
2286
+ /** Preenchido por `analyze()`. Vazio antes disso. */
2287
+ diagnostics = {
2288
+ filesScanned: 0,
2289
+ filesWithEvidence: 0,
2290
+ navigatorsDeclared: 0,
2291
+ navigatorsWithJsx: 0,
2292
+ routes: 0,
2293
+ routesLinkedToFile: 0
2294
+ };
1669
2295
  constructor(config, options) {
1670
2296
  this.config = config;
2297
+ this.graph = new ModuleGraph(config.rootDir);
1671
2298
  this.navigationInclude = options?.navigationInclude ?? [];
1672
2299
  this.navigationExclude = options?.navigationExclude ?? [];
1673
2300
  }
1674
2301
  /** Build the full navigation graph */
1675
2302
  async analyze() {
1676
2303
  const navigationFiles = await this.findNavigationFiles();
1677
- const parsedNavigators = [];
1678
2304
  const typeExports = [];
2305
+ this.routeTargets.clear();
2306
+ const declarations = /* @__PURE__ */ new Map();
2307
+ const evidenceFiles = [];
2308
+ this.navigatorFiles.clear();
1679
2309
  for (const filePath of navigationFiles) {
1680
2310
  try {
1681
2311
  const content = await fs$1.promises.readFile(filePath, "utf-8");
1682
- const navigators = this.parseNavigators(content, filePath);
1683
- const types = this.parseParamTypes(content, filePath);
1684
- parsedNavigators.push(...navigators);
1685
- typeExports.push(...types);
2312
+ if (!NAVIGATION_EVIDENCE.test(content)) continue;
2313
+ evidenceFiles.push(filePath);
2314
+ for (const decl of this.collectDeclarations(filePath)) {
2315
+ declarations.set(`${decl.file}#${decl.name}`, decl);
2316
+ if (decl.confident) this.navigatorFiles.add(decl.file);
2317
+ }
2318
+ typeExports.push(...this.parseParamTypes(content, filePath));
1686
2319
  } catch (error) {
1687
2320
  console.warn(`Failed to parse ${filePath}:`, error);
1688
2321
  }
1689
2322
  }
2323
+ const parsedNavigators = [];
2324
+ for (const filePath of evidenceFiles) {
2325
+ try {
2326
+ parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
2327
+ } catch (error) {
2328
+ console.warn(`Failed to parse navigators in ${filePath}:`, error);
2329
+ }
2330
+ }
2331
+ let detachedScreens = 0;
2332
+ for (const filePath of evidenceFiles) {
2333
+ try {
2334
+ for (const detached of this.parseDetachedScreens(filePath, declarations)) {
2335
+ const existing = parsedNavigators.find(
2336
+ (nav) => nav.name === detached.name && nav.type === detached.type
2337
+ );
2338
+ if (!existing) {
2339
+ parsedNavigators.push(detached);
2340
+ detachedScreens += detached.screens.length;
2341
+ continue;
2342
+ }
2343
+ const seen = new Set(existing.screens.map((screen) => screen.name));
2344
+ for (const screen of detached.screens) {
2345
+ if (seen.has(screen.name)) continue;
2346
+ seen.add(screen.name);
2347
+ existing.screens.push(screen);
2348
+ detachedScreens++;
2349
+ }
2350
+ }
2351
+ } catch (error) {
2352
+ console.warn(`Failed to parse detached screens in ${filePath}:`, error);
2353
+ }
2354
+ }
2355
+ const expo = analyzeExpoRouter(this.config.rootDir);
2356
+ if (expo) {
2357
+ console.log(
2358
+ `[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
2359
+ );
2360
+ this.diagnostics.fileBasedRouter = {
2361
+ kind: "expo-router",
2362
+ routeDir: expo.routeDir,
2363
+ routes: expo.routes.length
2364
+ };
2365
+ const byNavigator = /* @__PURE__ */ new Map();
2366
+ for (const nav of expo.navigators) {
2367
+ byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
2368
+ }
2369
+ for (const route of expo.routes) {
2370
+ const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
2371
+ name: route.navigatorName,
2372
+ type: route.navigatorType,
2373
+ screens: []
2374
+ }).get(route.navigatorName);
2375
+ navigator.screens.push({
2376
+ name: route.name,
2377
+ navigatorName: route.navigatorName,
2378
+ navigatorType: route.navigatorType,
2379
+ componentFile: route.componentFile,
2380
+ ...route.params.length ? {
2381
+ params: route.params.map((name) => ({ name, type: "string", required: true }))
2382
+ } : {}
2383
+ });
2384
+ this.routeTargets.set(route.name, route.componentFile);
2385
+ }
2386
+ parsedNavigators.push(...byNavigator.values());
2387
+ }
2388
+ let staticOnly = 0;
2389
+ for (const decl of declarations.values()) {
2390
+ if (!decl.confident || !decl.staticScreens?.length) continue;
2391
+ const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
2392
+ if (existing) {
2393
+ const seen = new Set(existing.screens.map((s) => s.name));
2394
+ for (const screen of decl.staticScreens) {
2395
+ if (!seen.has(screen.name)) existing.screens.push(screen);
2396
+ }
2397
+ continue;
2398
+ }
2399
+ parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
2400
+ staticOnly++;
2401
+ }
2402
+ const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
2403
+ const uniqueRoutes = new Set(
2404
+ parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
2405
+ );
2406
+ this.diagnostics = {
2407
+ ...this.diagnostics,
2408
+ filesScanned: navigationFiles.length,
2409
+ filesWithEvidence: evidenceFiles.length,
2410
+ navigatorsDeclared: confidentDeclarations,
2411
+ navigatorsWithJsx: parsedNavigators.length,
2412
+ routes: uniqueRoutes.size,
2413
+ // `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
2414
+ // registrada por um navegador que não sobreviveu. O mínimo evita a
2415
+ // cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
2416
+ routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
2417
+ };
2418
+ console.log(
2419
+ `[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`
2420
+ );
1690
2421
  this.attachParamsToNavigators(parsedNavigators, typeExports);
1691
2422
  return this.buildNavigationGraph(parsedNavigators);
1692
2423
  }
1693
- /** Find all navigation-related files */
2424
+ /** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
2425
+ collectDeclarations(filePath) {
2426
+ const ast = this.graph.parse(filePath);
2427
+ if (!ast) return [];
2428
+ const out = /* @__PURE__ */ new Map();
2429
+ traverse4__default.default(ast, {
2430
+ CallExpression: (nodePath) => {
2431
+ const callee = nodePath.node.callee;
2432
+ if (!BabelTypes__namespace.isIdentifier(callee)) return;
2433
+ const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
2434
+ const type = known ?? inferNavigatorType(callee.name);
2435
+ const confident = known !== null;
2436
+ let up = nodePath.parentPath;
2437
+ for (let i = 0; i < 4 && up; i++) {
2438
+ if (BabelTypes__namespace.isVariableDeclarator(up.node)) break;
2439
+ if (!BabelTypes__namespace.isMemberExpression(up.node) && !BabelTypes__namespace.isCallExpression(up.node)) {
2440
+ up = null;
2441
+ break;
2442
+ }
2443
+ up = up.parentPath;
2444
+ }
2445
+ const declarator = up?.node;
2446
+ if (!declarator || !BabelTypes__namespace.isVariableDeclarator(declarator) || !BabelTypes__namespace.isIdentifier(declarator.id)) {
2447
+ return;
2448
+ }
2449
+ const name = declarator.id.name;
2450
+ if (out.get(name)?.confident) return;
2451
+ const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
2452
+ out.set(name, {
2453
+ file: filePath,
2454
+ name,
2455
+ type,
2456
+ confident,
2457
+ ...staticScreens.length ? { staticScreens } : {}
2458
+ });
2459
+ }
2460
+ });
2461
+ return [...out.values()];
2462
+ }
2463
+ /**
2464
+ * As rotas de `createXNavigator({ screens: { … } })`.
2465
+ *
2466
+ * Quatro formas no corpus, todas em `rocketchat`:
2467
+ * `NewServerView` — shorthand, nome = componente
2468
+ * `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
2469
+ * `SelectListView: SelectListViewScreen` — identificador direto
2470
+ * `ChatsStackNavigator: ChatsStack` — navegador aninhado
2471
+ *
2472
+ * `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
2473
+ * e agrupar rotas não muda o que elas são.
2474
+ */
2475
+ parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
2476
+ if (!config || !BabelTypes__namespace.isObjectExpression(config) || depth > 4) return [];
2477
+ const out = [];
2478
+ for (const prop of config.properties) {
2479
+ if (!BabelTypes__namespace.isObjectProperty(prop)) continue;
2480
+ const key = BabelTypes__namespace.isIdentifier(prop.key) ? prop.key.name : BabelTypes__namespace.isStringLiteral(prop.key) ? prop.key.value : null;
2481
+ if (!key) continue;
2482
+ if (key === "groups" && BabelTypes__namespace.isObjectExpression(prop.value)) {
2483
+ for (const group of prop.value.properties) {
2484
+ if (!BabelTypes__namespace.isObjectProperty(group) || !BabelTypes__namespace.isExpression(group.value)) continue;
2485
+ out.push(
2486
+ ...this.parseStaticScreens(
2487
+ filePath,
2488
+ group.value,
2489
+ navigatorName,
2490
+ navigatorType,
2491
+ depth + 1
2492
+ )
2493
+ );
2494
+ }
2495
+ continue;
2496
+ }
2497
+ if (key !== "screens" || !BabelTypes__namespace.isObjectExpression(prop.value)) continue;
2498
+ for (const entry of prop.value.properties) {
2499
+ if (!BabelTypes__namespace.isObjectProperty(entry)) continue;
2500
+ const routeName = BabelTypes__namespace.isIdentifier(entry.key) ? entry.key.name : BabelTypes__namespace.isStringLiteral(entry.key) ? entry.key.value : null;
2501
+ if (!routeName) continue;
2502
+ const componentExpr = unwrapStaticScreen(entry.value);
2503
+ const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
2504
+ if (componentFile) this.routeTargets.set(routeName, componentFile);
2505
+ out.push({
2506
+ name: routeName,
2507
+ navigatorName,
2508
+ navigatorType,
2509
+ ...componentFile ? { componentFile } : {}
2510
+ });
2511
+ }
2512
+ }
2513
+ return out;
2514
+ }
2515
+ /**
2516
+ * Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
2517
+ * certo.
2518
+ *
2519
+ * POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
2520
+ * `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
2521
+ * convenção de caminho. Medido contra 20 apps React Native de terceiros
2522
+ * (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
2523
+ * app não ter navegação:
2524
+ *
2525
+ * - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
2526
+ * - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
2527
+ * sensível a caixa;
2528
+ * - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
2529
+ * pedia um arquivo chamado `routes*`;
2530
+ * - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
2531
+ * - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
2532
+ *
2533
+ * Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
2534
+ * globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
2535
+ * O que segura o custo aqui é o portão por evidência em `analyze()`, que só
2536
+ * parseia arquivo cujo texto menciona uma fábrica de navegador.
2537
+ *
2538
+ * Os globs antigos continuam na lista: se um projeto restringir `include`, o
2539
+ * que era encontrado antes continua sendo.
2540
+ */
1694
2541
  async findNavigationFiles() {
1695
2542
  const patterns = [
2543
+ ...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
2544
+ // Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
1696
2545
  "**/navigation/**/*.{ts,tsx}",
1697
2546
  "**/navigator*.{ts,tsx}",
1698
2547
  "**/routes*.{ts,tsx}",
@@ -1703,91 +2552,188 @@ var NavigationAnalyzer = class {
1703
2552
  "**/node_modules/**",
1704
2553
  "**/dist/**",
1705
2554
  "**/build/**",
2555
+ // Um navegador declarado dentro de um teste é fixture, não a navegação
2556
+ // do app. Isto passou a importar quando a varredura deixou de ser por
2557
+ // caminho: em `expensify`, os únicos arquivos que o glob antigo
2558
+ // alcançava eram três de `tests/`.
2559
+ "**/__tests__/**",
2560
+ "**/test/**",
2561
+ "**/tests/**",
2562
+ "**/*.test.{ts,tsx,js,jsx}",
2563
+ "**/*.tests.{ts,tsx,js,jsx}",
2564
+ "**/*.spec.{ts,tsx,js,jsx}",
2565
+ // Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
2566
+ // `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
2567
+ // Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
2568
+ // destinos que existem no teste e não no app, para os quais o agente
2569
+ // tentaria navegar.
2570
+ "**/*Test.{ts,tsx,js,jsx}",
2571
+ "**/*Tests.{ts,tsx,js,jsx}",
1706
2572
  ...this.config.exclude || [],
1707
2573
  // §5: Add user-configured navigation excludes
1708
2574
  ...this.navigationExclude
1709
2575
  ];
1710
- const files = await glob__default.default(patterns, {
2576
+ const files = await globSorted(patterns, {
1711
2577
  cwd: this.config.rootDir,
1712
2578
  ignore: excludePatterns
1713
2579
  });
1714
- return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
2580
+ return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
1715
2581
  }
1716
- /** Parse navigator definitions from a file */
1717
- parseNavigators(content, filePath) {
1718
- const navigators = [];
1719
- try {
1720
- const ast = parser__namespace.parse(content, {
1721
- sourceType: "module",
1722
- plugins: [
1723
- "jsx",
1724
- "typescript",
1725
- ...this.config.parserPlugins || []
1726
- ]
1727
- });
1728
- const navigatorCalls = /* @__PURE__ */ new Map();
1729
- const screensByNavigator = /* @__PURE__ */ new Map();
1730
- traverse4__default.default(ast, {
1731
- // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
1732
- CallExpression: (nodePath) => {
1733
- const { node } = nodePath;
1734
- const callee = node.callee;
1735
- let navigatorType = null;
1736
- if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
1737
- navigatorType = "stack";
1738
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createStackNavigator") {
1739
- navigatorType = "stack";
1740
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
1741
- navigatorType = "tab";
1742
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createTabNavigator") {
1743
- navigatorType = "tab";
1744
- } else if (BabelTypes__namespace.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
1745
- navigatorType = "drawer";
1746
- }
1747
- if (navigatorType) {
1748
- const parent = nodePath.parent;
1749
- if (BabelTypes__namespace.isVariableDeclarator(parent) && BabelTypes__namespace.isIdentifier(parent.id)) {
1750
- const varName = parent.id.name;
1751
- navigatorCalls.set(varName, {
1752
- name: varName,
1753
- type: navigatorType,
1754
- screens: []
1755
- });
1756
- }
1757
- }
1758
- },
1759
- // Detect Stack.Navigator / Tab.Navigator JSX elements
1760
- JSXElement: (nodePath) => {
1761
- const { node } = nodePath;
1762
- const openingElement = node.openingElement;
1763
- if (BabelTypes__namespace.isJSXMemberExpression(openingElement.name) && BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
1764
- const objectName = openingElement.name.object.name;
1765
- const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
1766
- if (propertyName === "Navigator") {
1767
- const navigator = navigatorCalls.get(objectName);
1768
- if (navigator) {
1769
- const initialRouteAttr = openingElement.attributes.find(
1770
- (attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
1771
- );
1772
- if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr) && BabelTypes__namespace.isStringLiteral(initialRouteAttr.value)) {
1773
- navigator.initialRouteName = initialRouteAttr.value.value;
1774
- }
1775
- const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
1776
- screensByNavigator.set(objectName, screens);
1777
- }
1778
- }
1779
- }
2582
+ /**
2583
+ * Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
2584
+ * que pode estar aqui ou em qualquer arquivo que este importe.
2585
+ */
2586
+ parseNavigatorUsages(filePath, declarations) {
2587
+ const ast = this.graph.parse(filePath);
2588
+ if (!ast) return [];
2589
+ const found = /* @__PURE__ */ new Map();
2590
+ traverse4__default.default(ast, {
2591
+ JSXElement: (nodePath) => {
2592
+ const { node } = nodePath;
2593
+ const openingElement = node.openingElement;
2594
+ if (!BabelTypes__namespace.isJSXMemberExpression(openingElement.name) || !BabelTypes__namespace.isJSXIdentifier(openingElement.name.object)) {
2595
+ return;
1780
2596
  }
1781
- });
1782
- navigatorCalls.forEach((navigator) => {
1783
- const screens = screensByNavigator.get(navigator.name) || [];
1784
- navigator.screens = screens;
1785
- navigators.push(navigator);
1786
- });
1787
- } catch (error) {
1788
- console.warn(`Failed to parse navigators in ${filePath}:`, error);
2597
+ const objectName = openingElement.name.object.name;
2598
+ const propertyName = BabelTypes__namespace.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2599
+ if (propertyName !== "Navigator") return;
2600
+ const decl = this.resolveNavigator(filePath, objectName, declarations);
2601
+ if (!decl) return;
2602
+ const navigator = found.get(objectName) ?? {
2603
+ name: objectName,
2604
+ type: decl.type,
2605
+ screens: []
2606
+ };
2607
+ const initialRouteAttr = openingElement.attributes.find(
2608
+ (attr) => BabelTypes__namespace.isJSXAttribute(attr) && BabelTypes__namespace.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2609
+ );
2610
+ if (BabelTypes__namespace.isJSXAttribute(initialRouteAttr)) {
2611
+ const initial = this.attributeString(filePath, initialRouteAttr);
2612
+ if (initial) navigator.initialRouteName = initial;
2613
+ }
2614
+ this.navigatorFiles.add(filePath);
2615
+ this.navigatorFiles.add(decl.file);
2616
+ navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
2617
+ found.set(objectName, navigator);
2618
+ }
2619
+ });
2620
+ return [...found.values()];
2621
+ }
2622
+ /**
2623
+ * Rotas declaradas FORA de qualquer `<X.Navigator>`.
2624
+ *
2625
+ * O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
2626
+ * que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
2627
+ * dois são 171 rotas invisíveis:
2628
+ *
2629
+ * `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
2630
+ * <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
2631
+ * dentro de seis navegadores diferentes. 70 rotas.
2632
+ * `comapeo` — `export const createAppScreens = ({intl}) => (<>
2633
+ * <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
2634
+ * num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
2635
+ *
2636
+ * A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
2637
+ * navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
2638
+ * literal do fonte (ou constante que o resolvedor segue), e `X` precisa
2639
+ * chegar a uma declaração de navegador que já existe. O que não resolve não
2640
+ * vira nada.
2641
+ *
2642
+ * O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
2643
+ * fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
2644
+ *
2645
+ * DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
2646
+ * primeira é a de sempre (declaração local, ou `import` seguido até a
2647
+ * origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
2648
+ * parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
2649
+ * o navegador `Flat` é o próprio app, no fonte.
2650
+ *
2651
+ * O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
2652
+ * contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
2653
+ * cinco stacks que também chamam a mesma função. Seguir os seis pontos de
2654
+ * chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
2655
+ * da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
2656
+ */
2657
+ parseDetachedScreens(filePath, declarations) {
2658
+ const ast = this.graph.parse(filePath);
2659
+ if (!ast) return [];
2660
+ const byNavigator = /* @__PURE__ */ new Map();
2661
+ const resolved = /* @__PURE__ */ new Map();
2662
+ traverse4__default.default(ast, {
2663
+ JSXElement: (nodePath) => {
2664
+ const name = nodePath.node.openingElement.name;
2665
+ if (!BabelTypes__namespace.isJSXMemberExpression(name) || !BabelTypes__namespace.isJSXIdentifier(name.object)) return;
2666
+ if (!BabelTypes__namespace.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
2667
+ const insideNavigator = nodePath.findParent((parent) => {
2668
+ if (!parent.isJSXElement()) return false;
2669
+ const parentName = parent.node.openingElement.name;
2670
+ return BabelTypes__namespace.isJSXMemberExpression(parentName) && BabelTypes__namespace.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
2671
+ });
2672
+ if (insideNavigator) return;
2673
+ const local = name.object.name;
2674
+ if (!resolved.has(local)) {
2675
+ resolved.set(
2676
+ local,
2677
+ this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
2678
+ );
2679
+ }
2680
+ const decl = resolved.get(local);
2681
+ if (!decl) return;
2682
+ const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
2683
+ if (!screen) return;
2684
+ const nav = byNavigator.get(decl.name) ?? {
2685
+ name: decl.name,
2686
+ type: decl.type,
2687
+ screens: []
2688
+ };
2689
+ if (!nav.screens.some((existing) => existing.name === screen.name))
2690
+ nav.screens.push(screen);
2691
+ byNavigator.set(decl.name, nav);
2692
+ }
2693
+ });
2694
+ return [...byNavigator.values()];
2695
+ }
2696
+ /**
2697
+ * De um `X` usado como `<X.Screen>` até a declaração do navegador.
2698
+ *
2699
+ * Além do caminho normal — declaração local ou `import` seguido até a origem
2700
+ * — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
2701
+ * `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
2702
+ */
2703
+ resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
2704
+ const direct = this.resolveNavigator(filePath, localName, declarations);
2705
+ if (direct) return direct;
2706
+ const binding = nodePath.scope.getBinding(localName);
2707
+ if (!binding || binding.kind !== "param") return null;
2708
+ const param = binding.path.node;
2709
+ if (!BabelTypes__namespace.isIdentifier(param) || !BabelTypes__namespace.isTSTypeAnnotation(param.typeAnnotation)) return null;
2710
+ const annotation = param.typeAnnotation.typeAnnotation;
2711
+ if (!BabelTypes__namespace.isTSTypeQuery(annotation) || !BabelTypes__namespace.isIdentifier(annotation.exprName)) return null;
2712
+ return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
2713
+ }
2714
+ /**
2715
+ * De `<X.Navigator>` até a declaração de `X`.
2716
+ *
2717
+ * Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
2718
+ * é definido. Resolver pelo import é o que torna a junção entre arquivos
2719
+ * segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
2720
+ * é o arquivo de DEFINIÇÃO.
2721
+ */
2722
+ resolveNavigator(filePath, localName, declarations) {
2723
+ const local = declarations.get(`${filePath}#${localName}`);
2724
+ if (local) return local;
2725
+ const origin = this.graph.resolveBinding(filePath, localName);
2726
+ if (!origin) return null;
2727
+ return declarations.get(`${origin.file}#${origin.name}`) ?? null;
2728
+ }
2729
+ /** O valor string de um atributo JSX, resolvendo constante importada. */
2730
+ attributeString(filePath, attr) {
2731
+ const value = attr.value;
2732
+ if (BabelTypes__namespace.isStringLiteral(value)) return value.value;
2733
+ if (BabelTypes__namespace.isJSXExpressionContainer(value) && BabelTypes__namespace.isExpression(value.expression)) {
2734
+ return this.graph.stringConstant(filePath, value.expression);
1789
2735
  }
1790
- return navigators;
2736
+ return null;
1791
2737
  }
1792
2738
  /**
1793
2739
  * Is this JSX element `<navigatorVarName.MEMBER …>`?
@@ -1882,22 +2828,48 @@ var NavigationAnalyzer = class {
1882
2828
  return found;
1883
2829
  }
1884
2830
  /** Extract screens from a navigator JSX element */
1885
- extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
2831
+ extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
1886
2832
  const screens = [];
1887
2833
  if (!navigatorElement.children) return screens;
1888
2834
  const seen = /* @__PURE__ */ new Set();
1889
2835
  for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
1890
- const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
1891
- if (!screenName || seen.has(screenName)) continue;
1892
- seen.add(screenName);
1893
- screens.push({
1894
- name: screenName,
1895
- navigatorName: navigatorVarName,
1896
- navigatorType
1897
- });
2836
+ const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
2837
+ if (!screen || seen.has(screen.name)) continue;
2838
+ seen.add(screen.name);
2839
+ screens.push(screen);
1898
2840
  }
1899
2841
  return screens;
1900
2842
  }
2843
+ /**
2844
+ * Uma `<X.Screen>` isolada até a rota que ela declara.
2845
+ *
2846
+ * Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
2847
+ * a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
2848
+ * dele — só a forma de chegar até o elemento muda.
2849
+ */
2850
+ parseScreenElement(filePath, element, navigatorName, navigatorType) {
2851
+ const attrs = element.openingElement.attributes;
2852
+ const nameAttr = attrs.find(
2853
+ (a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "name"
2854
+ );
2855
+ const screenName = BabelTypes__namespace.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
2856
+ if (!screenName) return null;
2857
+ const componentAttr = attrs.find(
2858
+ (a) => BabelTypes__namespace.isJSXAttribute(a) && BabelTypes__namespace.isJSXIdentifier(a.name) && a.name.name === "component"
2859
+ );
2860
+ let componentFile = null;
2861
+ if (BabelTypes__namespace.isJSXAttribute(componentAttr) && BabelTypes__namespace.isJSXExpressionContainer(componentAttr.value)) {
2862
+ const expr = componentAttr.value.expression;
2863
+ if (BabelTypes__namespace.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
2864
+ }
2865
+ if (componentFile) this.routeTargets.set(screenName, componentFile);
2866
+ return {
2867
+ name: screenName,
2868
+ navigatorName,
2869
+ navigatorType,
2870
+ ...componentFile ? { componentFile } : {}
2871
+ };
2872
+ }
1901
2873
  /** Extract string attribute value from JSX attributes */
1902
2874
  extractAttributeValue(attributes, attrName) {
1903
2875
  const attr = attributes.find(
@@ -1993,7 +2965,7 @@ var NavigationAnalyzer = class {
1993
2965
  if (type.type === "TSUndefinedKeyword") return "undefined";
1994
2966
  if (type.type === "TSNullKeyword") return "null";
1995
2967
  if (type.type === "TSUnionType") {
1996
- return type.types.map((t12) => this.typeToString(t12)).join(" | ");
2968
+ return type.types.map((t13) => this.typeToString(t13)).join(" | ");
1997
2969
  }
1998
2970
  if (type.type === "TSTypeLiteral") {
1999
2971
  return "object";
@@ -2014,7 +2986,7 @@ var NavigationAnalyzer = class {
2014
2986
  /** Attach parsed type params to navigator screens */
2015
2987
  attachParamsToNavigators(navigators, types) {
2016
2988
  for (const navigator of navigators) {
2017
- const matchingType = types.find((t12) => t12.type === navigator.type);
2989
+ const matchingType = types.find((t13) => t13.type === navigator.type);
2018
2990
  if (matchingType) {
2019
2991
  for (const screen of navigator.screens) {
2020
2992
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -2104,8 +3076,8 @@ var ComponentAnalyzer = class {
2104
3076
  });
2105
3077
  const components = [];
2106
3078
  traverse4__default.default(ast, {
2107
- JSXElement: (path9) => {
2108
- const component = this.extractComponentFromJSXElement(path9.node);
3079
+ JSXElement: (path11) => {
3080
+ const component = this.extractComponentFromJSXElement(path11.node);
2109
3081
  if (component) {
2110
3082
  components.push(component);
2111
3083
  }
@@ -2232,13 +3204,13 @@ var FormAnalyzer = class {
2232
3204
  this.inputElements = [];
2233
3205
  this.submitButtons = [];
2234
3206
  traverse4__default.default(ast, {
2235
- CallExpression: (path9) => {
2236
- this.extractStateVariables(path9.node);
3207
+ CallExpression: (path11) => {
3208
+ this.extractStateVariables(path11.node);
2237
3209
  }
2238
3210
  });
2239
3211
  traverse4__default.default(ast, {
2240
- JSXElement: (path9) => {
2241
- this.extractFormElements(path9.node);
3212
+ JSXElement: (path11) => {
3213
+ this.extractFormElements(path11.node);
2242
3214
  }
2243
3215
  });
2244
3216
  const validationRules = this.extractValidationRules(ast);
@@ -2338,8 +3310,8 @@ var FormAnalyzer = class {
2338
3310
  extractValidationRules(ast) {
2339
3311
  const rules = {};
2340
3312
  traverse4__default.default(ast, {
2341
- IfStatement: (path9) => {
2342
- const test = path9.node.test;
3313
+ IfStatement: (path11) => {
3314
+ const test = path11.node.test;
2343
3315
  const rule = this.extractRuleFromCondition(test);
2344
3316
  if (rule) {
2345
3317
  const { field, description } = rule;
@@ -2393,7 +3365,7 @@ var FormAnalyzer = class {
2393
3365
  }
2394
3366
  buildForms(filePath, validationRules) {
2395
3367
  if (this.inputElements.length === 0) return [];
2396
- const fileName = path8__namespace.basename(filePath, path8__namespace.extname(filePath));
3368
+ const fileName = path2__namespace.basename(filePath, path2__namespace.extname(filePath));
2397
3369
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2398
3370
  const fields = this.inputElements.map((input) => {
2399
3371
  const fieldType = this.inferFieldType(input);
@@ -2442,13 +3414,105 @@ var FormAnalyzer = class {
2442
3414
  return rule !== void 0 && rule.includes("required");
2443
3415
  }
2444
3416
  };
3417
+
3418
+ // src/pipeline/composition.ts
3419
+ var MAX_DEPTH = 3;
3420
+ var MAX_VISITED_PER_SCREEN = 60;
3421
+ var MAX_FAN_IN = 3;
3422
+ var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
3423
+ function descendants(graph, from, screenFiles) {
3424
+ const seen = /* @__PURE__ */ new Set([from]);
3425
+ const out = [];
3426
+ let frontier = [from];
3427
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
3428
+ const next = [];
3429
+ for (const file of frontier) {
3430
+ for (const child of graph.renderedComponentFiles(file)) {
3431
+ if (seen.has(child)) continue;
3432
+ seen.add(child);
3433
+ if (screenFiles.has(child)) continue;
3434
+ out.push(child);
3435
+ next.push(child);
3436
+ if (out.length >= MAX_VISITED_PER_SCREEN) return out;
3437
+ }
3438
+ }
3439
+ frontier = next;
3440
+ }
3441
+ return out;
3442
+ }
3443
+ function addressable(locator) {
3444
+ if (!locator || typeof locator !== "object") return false;
3445
+ const l = locator;
3446
+ if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
3447
+ return typeof l["testID"] === "string" && l["testID"].length > 0;
3448
+ }
3449
+ async function composeAffordances(options) {
3450
+ const { screens, graph, analyzeFile, screenFiles } = options;
3451
+ const byScreen = /* @__PURE__ */ new Map();
3452
+ const fanIn = /* @__PURE__ */ new Map();
3453
+ for (const screen of screens) {
3454
+ if (!screen.filePath) continue;
3455
+ const children = descendants(graph, screen.filePath, screenFiles);
3456
+ byScreen.set(screen, children);
3457
+ for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
3458
+ }
3459
+ const analyzed = /* @__PURE__ */ new Map();
3460
+ let screensEnriched = 0;
3461
+ let filesMerged = 0;
3462
+ for (const [screen, children] of byScreen) {
3463
+ const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
3464
+ if (exclusive.length === 0) continue;
3465
+ const actionIds = new Set(screen.actions.map((a) => a.id));
3466
+ const targetIds = new Set((screen.targets ?? []).map((t13) => t13.id));
3467
+ const formIds = new Set(screen.forms.map((f) => f.id));
3468
+ let gained = false;
3469
+ for (const file of exclusive) {
3470
+ if (!analyzed.has(file)) {
3471
+ try {
3472
+ analyzed.set(file, await analyzeFile(file));
3473
+ } catch {
3474
+ analyzed.set(file, null);
3475
+ }
3476
+ }
3477
+ const child = analyzed.get(file);
3478
+ if (!child) continue;
3479
+ let merged = false;
3480
+ for (const action of child.actions) {
3481
+ if (!addressable(action.locator)) continue;
3482
+ if (actionIds.has(action.id)) continue;
3483
+ actionIds.add(action.id);
3484
+ screen.actions.push(action);
3485
+ merged = true;
3486
+ }
3487
+ for (const target of child.targets ?? []) {
3488
+ if (!addressable(target.locator)) continue;
3489
+ if (targetIds.has(target.id)) continue;
3490
+ targetIds.add(target.id);
3491
+ (screen.targets ??= []).push(target);
3492
+ merged = true;
3493
+ }
3494
+ for (const form of child.forms) {
3495
+ if (form.fields.length === 0) continue;
3496
+ const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
3497
+ if (formIds.has(id)) continue;
3498
+ formIds.add(id);
3499
+ screen.forms.push({ ...form, id });
3500
+ merged = true;
3501
+ }
3502
+ if (merged) {
3503
+ filesMerged++;
3504
+ gained = true;
3505
+ }
3506
+ }
3507
+ if (gained) screensEnriched++;
3508
+ }
3509
+ return { screens, screensEnriched, filesMerged };
3510
+ }
3511
+
3512
+ // src/analyzers/ReactNativePlatformAnalyzer.ts
2445
3513
  var ReactNativePlatformAnalyzer = class {
2446
3514
  platform = "react-native";
2447
3515
  async analyze(config, options) {
2448
- const screenAnalyzer = new ScreenAnalyzer(config, {
2449
- strictScreens: options.strictScreens ?? true,
2450
- screenPatterns: options.screenPatterns
2451
- });
2452
3516
  const navigationAnalyzer = new NavigationAnalyzer(config, {
2453
3517
  navigationInclude: options.navigationInclude,
2454
3518
  navigationExclude: options.navigationExclude
@@ -2456,14 +3520,31 @@ var ReactNativePlatformAnalyzer = class {
2456
3520
  const componentAnalyzer = new ComponentAnalyzer(config);
2457
3521
  const formAnalyzer = new FormAnalyzer(config);
2458
3522
  console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
2459
- const [screens, navigation] = await Promise.all([
2460
- screenAnalyzer.analyze(),
2461
- navigationAnalyzer.analyze()
2462
- ]);
3523
+ const navigation = await navigationAnalyzer.analyze();
3524
+ const screenAnalyzer = new ScreenAnalyzer(config, {
3525
+ strictScreens: options.strictScreens ?? true,
3526
+ screenPatterns: options.screenPatterns,
3527
+ // Menos os arquivos que declaram navegador: ver `navigatorFiles`.
3528
+ routeTargetFiles: new Set(
3529
+ [...navigationAnalyzer.routeTargets.values()].filter(
3530
+ (file) => !navigationAnalyzer.navigatorFiles.has(file)
3531
+ )
3532
+ )
3533
+ });
3534
+ const screens = await screenAnalyzer.analyze();
3535
+ const composed = await composeAffordances({
3536
+ screens,
3537
+ graph: navigationAnalyzer.graph,
3538
+ analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
3539
+ screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
3540
+ });
3541
+ console.log(
3542
+ `[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
3543
+ );
2463
3544
  console.log(
2464
3545
  `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
2465
3546
  );
2466
- const screenFiles = await glob__default.default(config.include || ["**/*.tsx", "**/*.ts"], {
3547
+ const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
2467
3548
  cwd: config.rootDir,
2468
3549
  ignore: config.exclude || ["**/node_modules/**"]
2469
3550
  });
@@ -2471,7 +3552,7 @@ var ReactNativePlatformAnalyzer = class {
2471
3552
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
2472
3553
  );
2473
3554
  const enrichmentPromises = screenFiles.map(async (file) => {
2474
- const filePath = path8__namespace.default.resolve(config.rootDir, file);
3555
+ const filePath = path2__namespace.default.resolve(config.rootDir, file);
2475
3556
  try {
2476
3557
  const [components, forms] = await Promise.all([
2477
3558
  componentAnalyzer.analyzeFile(filePath),
@@ -2526,7 +3607,8 @@ var ReactNativePlatformAnalyzer = class {
2526
3607
  screens: enrichedScreens,
2527
3608
  navigation,
2528
3609
  analyzedFiles: screenFiles.length,
2529
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
3610
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
3611
+ diagnostics: { navigation: navigationAnalyzer.diagnostics }
2530
3612
  };
2531
3613
  }
2532
3614
  mergeForm(target, source) {
@@ -2544,9 +3626,12 @@ var ReactNativePlatformAnalyzer = class {
2544
3626
  findEquivalentField(fields, incoming) {
2545
3627
  return fields.find((field) => {
2546
3628
  if (field.name && incoming.name && field.name === incoming.name) return true;
2547
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2548
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2549
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
3629
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
3630
+ return true;
3631
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
3632
+ return true;
3633
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
3634
+ return true;
2550
3635
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2551
3636
  return true;
2552
3637
  }
@@ -2579,7 +3664,9 @@ var ReactNativePlatformAnalyzer = class {
2579
3664
  if (incoming.fields.length === 0) return void 0;
2580
3665
  let best;
2581
3666
  for (const form of forms) {
2582
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
3667
+ const overlap = incoming.fields.filter(
3668
+ (field) => this.findEquivalentField(form.fields, field)
3669
+ ).length;
2583
3670
  if (overlap > 0 && (!best || overlap > best.overlap)) {
2584
3671
  best = { form, overlap };
2585
3672
  }
@@ -2733,13 +3820,13 @@ function staticRoutePath(node) {
2733
3820
  if (!node) return void 0;
2734
3821
  if (BabelTypes__namespace.isStringLiteral(node)) return node.value;
2735
3822
  if (BabelTypes__namespace.isTemplateLiteral(node)) {
2736
- let path9 = "";
3823
+ let path11 = "";
2737
3824
  node.quasis.forEach((quasi, index) => {
2738
- path9 += quasi.value.cooked ?? quasi.value.raw;
3825
+ path11 += quasi.value.cooked ?? quasi.value.raw;
2739
3826
  const expr = node.expressions[index];
2740
- if (expr) path9 += `:${paramNameOf(expr)}`;
3827
+ if (expr) path11 += `:${paramNameOf(expr)}`;
2741
3828
  });
2742
- return path9;
3829
+ return path11;
2743
3830
  }
2744
3831
  return void 0;
2745
3832
  }
@@ -2824,17 +3911,17 @@ var WebScreenAnalyzer = class {
2824
3911
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2825
3912
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2826
3913
  } = this.config;
2827
- const files = (await glob__default.default(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
3914
+ const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2828
3915
  (file) => !file.endsWith(".d.ts")
2829
3916
  );
2830
- const patternMatches = await glob__default.default(this.screenPatterns, {
3917
+ const patternMatches = await globSorted(this.screenPatterns, {
2831
3918
  cwd: this.config.rootDir,
2832
3919
  ignore: exclude
2833
3920
  });
2834
- const patternSet = new Set(patternMatches.map((f) => path8__namespace.default.resolve(this.config.rootDir, f)));
3921
+ const patternSet = new Set(patternMatches.map((f) => path2__namespace.default.resolve(this.config.rootDir, f)));
2835
3922
  const candidates = [];
2836
3923
  for (const file of files) {
2837
- const filePath = path8__namespace.default.resolve(this.config.rootDir, file);
3924
+ const filePath = path2__namespace.default.resolve(this.config.rootDir, file);
2838
3925
  try {
2839
3926
  const candidate = await this.analyzeFile(filePath);
2840
3927
  if (!candidate) continue;
@@ -2878,7 +3965,7 @@ var WebScreenAnalyzer = class {
2878
3965
  }
2879
3966
  }
2880
3967
  }
2881
- const name = registerScreenMeta?.name || componentName || path8__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
3968
+ const name = registerScreenMeta?.name || componentName || path2__namespace.default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
2882
3969
  const descriptor = {
2883
3970
  name,
2884
3971
  filePath,
@@ -3767,7 +4854,9 @@ var WebNavigationAnalyzer = class {
3767
4854
  for (const filePath of files) {
3768
4855
  try {
3769
4856
  const content = await fs$1.promises.readFile(filePath, "utf-8");
3770
- if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
4857
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
4858
+ content
4859
+ )) {
3771
4860
  continue;
3772
4861
  }
3773
4862
  const ast = parseSource(content, this.config.parserPlugins);
@@ -3797,8 +4886,8 @@ var WebNavigationAnalyzer = class {
3797
4886
  ...this.config.exclude || [],
3798
4887
  ...this.navigationExclude
3799
4888
  ];
3800
- const files = await glob__default.default(patterns, { cwd: this.config.rootDir, ignore });
3801
- return files.map((file) => path8__namespace.default.join(this.config.rootDir, file));
4889
+ const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
4890
+ return files.map((file) => path2__namespace.default.join(this.config.rootDir, file));
3802
4891
  }
3803
4892
  // ── JSX <Route> style ────────────────────────────────────────────
3804
4893
  extractJsxRoutes(ast) {
@@ -3970,11 +5059,13 @@ var WebNavigationAnalyzer = class {
3970
5059
  return {
3971
5060
  screens,
3972
5061
  initialScreen: initialRoute?.screenName ?? "",
3973
- navigators: routes.length > 0 ? [{
3974
- name: navigatorName,
3975
- type: WEB_NAVIGATOR_TYPE,
3976
- screens: screenNames
3977
- }] : []
5062
+ navigators: routes.length > 0 ? [
5063
+ {
5064
+ name: navigatorName,
5065
+ type: WEB_NAVIGATOR_TYPE,
5066
+ screens: screenNames
5067
+ }
5068
+ ] : []
3978
5069
  };
3979
5070
  }
3980
5071
  };
@@ -4296,8 +5387,8 @@ var ZodParsedType = util.arrayToEnum([
4296
5387
  "set"
4297
5388
  ]);
4298
5389
  var getParsedType = (data) => {
4299
- const t12 = typeof data;
4300
- switch (t12) {
5390
+ const t13 = typeof data;
5391
+ switch (t13) {
4301
5392
  case "undefined":
4302
5393
  return ZodParsedType.undefined;
4303
5394
  case "string":
@@ -4569,8 +5660,8 @@ function getErrorMap() {
4569
5660
 
4570
5661
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4571
5662
  var makeIssue = (params) => {
4572
- const { data, path: path9, errorMaps, issueData } = params;
4573
- const fullPath = [...path9, ...issueData.path || []];
5663
+ const { data, path: path11, errorMaps, issueData } = params;
5664
+ const fullPath = [...path11, ...issueData.path || []];
4574
5665
  const fullIssue = {
4575
5666
  ...issueData,
4576
5667
  path: fullPath
@@ -4686,11 +5777,11 @@ var errorUtil;
4686
5777
 
4687
5778
  // ../../node_modules/zod/v3/types.js
4688
5779
  var ParseInputLazyPath = class {
4689
- constructor(parent, value, path9, key) {
5780
+ constructor(parent, value, path11, key) {
4690
5781
  this._cachedPath = [];
4691
5782
  this.parent = parent;
4692
5783
  this.data = value;
4693
- this._path = path9;
5784
+ this._path = path11;
4694
5785
  this._key = key;
4695
5786
  }
4696
5787
  get path() {
@@ -8131,7 +9222,18 @@ var coerce = {
8131
9222
  };
8132
9223
  var NEVER = INVALID;
8133
9224
 
8134
- // ../shared/dist/chunk-PKS3VDNB.mjs
9225
+ // ../shared/dist/chunk-SWQOZWHP.mjs
9226
+ var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
9227
+ var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
9228
+ function isSafeImageUrl(value) {
9229
+ if (value === null || value === void 0) return true;
9230
+ const trimmed = value.trim();
9231
+ if (trimmed === "") return true;
9232
+ if (!HAS_SCHEME_RE.test(trimmed)) return true;
9233
+ return SAFE_IMAGE_URL_RE.test(trimmed);
9234
+ }
9235
+
9236
+ // ../shared/dist/chunk-NMQNNPSJ.mjs
8135
9237
  var locatorSourceSchema = external_exports.enum([
8136
9238
  "appilotsId",
8137
9239
  "testID",
@@ -8310,6 +9412,23 @@ var actionDescriptorSchema = external_exports.object({
8310
9412
  effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
8311
9413
  riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
8312
9414
  requiresConfirmation: external_exports.boolean().optional(),
9415
+ /**
9416
+ * How long this action's work is expected to take, in milliseconds,
9417
+ * DECLARED by the app — not inferred (that is `appilotsInferred`).
9418
+ *
9419
+ * The SDK's post-action wait was a constant: 6s, or 10s when the
9420
+ * generator could prove the handler awaits something. No constant
9421
+ * fits, because app operations run from ~100ms to minutes, and the
9422
+ * failure is silent in both directions — too short and the agent
9423
+ * photographs a loading screen with no controls on it, too long and
9424
+ * every fast press pays for the slowest one. Only the app knows.
9425
+ *
9426
+ * Absent means absent: the SDK keeps its current defaults, so a
9427
+ * document generated before this field existed behaves exactly as it
9428
+ * did. The SDK also clamps the value to its own safety ceiling — a
9429
+ * declared budget is a request, not a licence to hang the session.
9430
+ */
9431
+ asyncBudgetMs: external_exports.number().int().positive().optional(),
8313
9432
  appilotsInferred: appilotsInferredActionSchema.optional()
8314
9433
  }).passthrough();
8315
9434
  var screenPermissionDescriptorSchema = external_exports.object({
@@ -8457,9 +9576,16 @@ var createProjectSchema = external_exports.object({
8457
9576
  // Reference to a tenant-scoped provider (preferred). Optional at
8458
9577
  // creation so a project can be created and configured later.
8459
9578
  providerId: external_exports.string().min(1).optional(),
8460
- systemPrompt: external_exports.string().max(4096).optional()
9579
+ // 16k chars (~4k tokens): um prompt de CS real não cabe em 4096 — o
9580
+ // primeiro cliente de produção chegou com 6.385 e tomou 'Invalid input'
9581
+ // sem saber por quê. O teto continua existindo porque o systemPrompt
9582
+ // entra em TODO hop do relay: é proteção de TPM, não de gosto.
9583
+ systemPrompt: external_exports.string().max(16e3).optional()
9584
+ });
9585
+ createProjectSchema.partial().extend({
9586
+ providerId: external_exports.string().min(1).nullable().optional(),
9587
+ systemPrompt: external_exports.string().max(16e3).nullable().optional()
8461
9588
  });
8462
- createProjectSchema.partial();
8463
9589
  var providerKindSchema = external_exports.enum(["openai", "anthropic", "custom"]);
8464
9590
  external_exports.object({
8465
9591
  name: external_exports.string().min(1).max(100),
@@ -8485,7 +9611,7 @@ external_exports.object({
8485
9611
  version: external_exports.string().default("1.0"),
8486
9612
  content: external_exports.record(external_exports.unknown())
8487
9613
  });
8488
- var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
9614
+ var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
8489
9615
  var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
8490
9616
  external_exports.object({
8491
9617
  name: external_exports.string().min(1).max(100),
@@ -8496,8 +9622,8 @@ external_exports.object({
8496
9622
  scope: apiKeyScopeSchema.default("sdk"),
8497
9623
  environment: apiKeyEnvironmentSchema.optional(),
8498
9624
  expiresAt: external_exports.string().datetime().optional()
8499
- }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
8500
- message: 'projectId is required for scope "sdk"',
9625
+ }).refine((v) => v.scope === "operator" || !!v.projectId, {
9626
+ message: "projectId is required for SDK and publishing keys",
8501
9627
  path: ["projectId"]
8502
9628
  });
8503
9629
  var boundedString = (max) => external_exports.string().max(max);
@@ -8522,6 +9648,15 @@ var snapshotInputSchema = external_exports.object({
8522
9648
  type: boundedString(40).optional(),
8523
9649
  required: external_exports.boolean().optional(),
8524
9650
  invalid: external_exports.boolean().optional(),
9651
+ /**
9652
+ * Whether the field holds anything, as its own fact rather than an
9653
+ * inference over `value`. The value is the user's and is the first
9654
+ * thing a privacy policy withholds; the existence of a value is the
9655
+ * agent's and is what stops it filling the same field twice.
9656
+ */
9657
+ filled: external_exports.boolean().optional(),
9658
+ /** This input has keyboard focus right now. */
9659
+ focused: external_exports.boolean().optional(),
8525
9660
  inModal: external_exports.boolean().optional()
8526
9661
  }).passthrough();
8527
9662
  var snapshotButtonSchema = external_exports.object({
@@ -8677,6 +9812,32 @@ var agentSnapshotSchema = external_exports.object({
8677
9812
  * it. Optional: older SDKs never clamp and never send it.
8678
9813
  */
8679
9814
  truncated: external_exports.boolean().optional(),
9815
+ /**
9816
+ * What moved since the previous observation, computed on the device
9817
+ * because that is the only side holding both snapshots.
9818
+ *
9819
+ * Shape only — counts, booleans, and ids the app declared itself — so
9820
+ * it survives an observation whose content was withheld. ABSENT means
9821
+ * there was no previous observation to compare against; `unchanged:
9822
+ * true` means we compared and nothing moved, which is the strongest
9823
+ * evidence available that an action did nothing. The two must not be
9824
+ * collapsed, for the same reason `truncated` exists.
9825
+ */
9826
+ delta: external_exports.object({
9827
+ routeChanged: external_exports.boolean().optional(),
9828
+ modalOpened: external_exports.boolean().optional(),
9829
+ modalClosed: external_exports.boolean().optional(),
9830
+ loadingStarted: external_exports.boolean().optional(),
9831
+ loadingFinished: external_exports.boolean().optional(),
9832
+ textsAdded: external_exports.number().int().nonnegative().optional(),
9833
+ textsRemoved: external_exports.number().int().nonnegative().optional(),
9834
+ visibleRowsDelta: external_exports.number().int().optional(),
9835
+ totalRowsDelta: external_exports.number().int().optional(),
9836
+ fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
9837
+ fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
9838
+ invalidAppeared: external_exports.boolean().optional(),
9839
+ unchanged: external_exports.boolean().optional()
9840
+ }).passthrough().optional(),
8680
9841
  /**
8681
9842
  * How many of this screen's controls the client could name, split by
8682
9843
  * `identityProvenance`. Diagnostic only — the relay never grounds an
@@ -8710,7 +9871,19 @@ var agentContextSchema = external_exports.object({
8710
9871
  rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8711
9872
  currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8712
9873
  routeNames: external_exports.array(boundedString(200)).max(500).optional(),
8713
- canGoBack: external_exports.boolean().optional()
9874
+ canGoBack: external_exports.boolean().optional(),
9875
+ /**
9876
+ * The stack a back press pops through, oldest first, ending on
9877
+ * the current screen. `canGoBack` says a back exists; this says
9878
+ * where it goes.
9879
+ */
9880
+ backStack: external_exports.array(boundedString(200)).max(50).optional(),
9881
+ /**
9882
+ * Screens the session has been on, oldest first. Names only —
9883
+ * params carry record ids and often personal data, and a route
9884
+ * name is structure.
9885
+ */
9886
+ visited: external_exports.array(boundedString(200)).max(8).optional()
8714
9887
  }).passthrough().optional(),
8715
9888
  screenMetadata: external_exports.object({
8716
9889
  name: boundedString(200).optional(),
@@ -8816,7 +9989,7 @@ var formFillPayloadSchema = external_exports.object({
8816
9989
  submitAfterFill: external_exports.boolean().optional()
8817
9990
  }).passthrough();
8818
9991
  var uiInteractionPayloadSchema = external_exports.object({
8819
- action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
9992
+ action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
8820
9993
  // `targetId` is the canonical server/LLM field. SDK runtimes still
8821
9994
  // accept `componentId` as a compatibility alias.
8822
9995
  targetId: external_exports.string().min(1),
@@ -8984,8 +10157,15 @@ external_exports.object({
8984
10157
  // issue #169 — the observation the server grounds targets against, now
8985
10158
  // validated + bounded at the border instead of z.record(z.unknown()).
8986
10159
  context: agentContextSchema.optional(),
8987
- /** Hop counter — server enforces a cap to prevent runaway loops */
8988
- hop: external_exports.number().int().min(1).max(10).optional()
10160
+ /**
10161
+ * Hop counter — server enforces a cap to prevent runaway loops.
10162
+ * The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
10163
+ * #503, apps/api/src/modules/agents/routes.ts): the first over-budget
10164
+ * hop has to get through validation so the route can answer it with
10165
+ * the friendly automation-limit message instead of a 422. The +3
10166
+ * headroom mirrors what 7/10 was before the recalibration.
10167
+ */
10168
+ hop: external_exports.number().int().min(1).max(13).optional()
8989
10169
  });
8990
10170
  var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
8991
10171
  var screenPermissionSchema = external_exports.object({
@@ -9091,15 +10271,18 @@ var themeTokensSchema = external_exports.object({
9091
10271
  */
9092
10272
  mode: external_exports.enum(["auto", "light", "dark"]).optional()
9093
10273
  }).strict();
10274
+ var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
10275
+ message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
10276
+ });
9094
10277
  external_exports.object({
9095
10278
  /** Tone / persona instructions appended to the system prompt. */
9096
10279
  personaPrompt: external_exports.string().max(2048).nullable(),
9097
10280
  /** Display name in the chat header (e.g. "Aria"). */
9098
10281
  assistantName: external_exports.string().max(60).nullable(),
9099
10282
  /** URL or remote asset id for the avatar shown next to assistant turns. */
9100
- assistantAvatar: external_exports.string().max(500).nullable(),
10283
+ assistantAvatar: imageSourceField.nullable(),
9101
10284
  /** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
9102
- emptyStateIcon: external_exports.string().max(500).nullable(),
10285
+ emptyStateIcon: imageSourceField.nullable(),
9103
10286
  /** First-message text shown in the empty state. */
9104
10287
  welcomeMessage: external_exports.string().max(500).nullable(),
9105
10288
  /** Title rendered at the top of the chat. */
@@ -9113,20 +10296,20 @@ external_exports.object({
9113
10296
  /** Chat-open FAB background color. Null falls back to theme.colors.primary. */
9114
10297
  triggerButtonColor: hexColorSchema.nullable(),
9115
10298
  /** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
9116
- triggerButtonImageUrl: external_exports.string().max(500).nullable()
10299
+ triggerButtonImageUrl: imageSourceField.nullable()
9117
10300
  });
9118
10301
  external_exports.object({
9119
10302
  personaPrompt: external_exports.string().max(2048).nullable().optional(),
9120
10303
  assistantName: external_exports.string().max(60).nullable().optional(),
9121
- assistantAvatar: external_exports.string().max(500).nullable().optional(),
9122
- emptyStateIcon: external_exports.string().max(500).nullable().optional(),
10304
+ assistantAvatar: imageSourceField.nullable().optional(),
10305
+ emptyStateIcon: imageSourceField.nullable().optional(),
9123
10306
  welcomeMessage: external_exports.string().max(500).nullable().optional(),
9124
10307
  chatTitle: external_exports.string().max(60).nullable().optional(),
9125
10308
  poweredByVisible: external_exports.boolean().optional(),
9126
10309
  theme: themeTokensSchema.nullable().optional(),
9127
10310
  defaultLocale: localeSchema.nullable().optional(),
9128
10311
  triggerButtonColor: hexColorSchema.nullable().optional(),
9129
- triggerButtonImageUrl: external_exports.string().max(500).nullable().optional()
10312
+ triggerButtonImageUrl: imageSourceField.nullable().optional()
9130
10313
  }).strict();
9131
10314
  var sandboxObservationSchema = external_exports.object({
9132
10315
  route: external_exports.string().max(200).optional(),
@@ -9146,7 +10329,19 @@ var sandboxObservationSchema = external_exports.object({
9146
10329
  type: external_exports.string().max(40).optional(),
9147
10330
  placeholder: external_exports.string().max(200).optional(),
9148
10331
  /** Inline validation state (SDK snapshot field). */
9149
- invalid: external_exports.boolean().optional()
10332
+ invalid: external_exports.boolean().optional(),
10333
+ /**
10334
+ * Whether the field holds anything, as its own fact rather than
10335
+ * an inference over `value`.
10336
+ *
10337
+ * Here because the eval posts through this schema: a fact the
10338
+ * SDK produces and this contract has no word for is a fact the
10339
+ * corpus can never grade the agent on. `invalid` and `required`
10340
+ * were already here; these two were the half that was missing.
10341
+ */
10342
+ filled: external_exports.boolean().optional(),
10343
+ /** This input has keyboard focus right now. */
10344
+ focused: external_exports.boolean().optional()
9150
10345
  })
9151
10346
  ).max(100).optional(),
9152
10347
  buttons: external_exports.array(
@@ -9625,7 +10820,7 @@ var manifestSchema = external_exports.object({
9625
10820
  navigation: navigationGraphSchema.partial().optional()
9626
10821
  }).passthrough();
9627
10822
  async function loadManifest(rootDir, manifestPath) {
9628
- const resolvedPath = path8__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
10823
+ const resolvedPath = path2__namespace.default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
9629
10824
  let raw;
9630
10825
  try {
9631
10826
  raw = await fs.readFile(resolvedPath, "utf-8");
@@ -9708,6 +10903,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
9708
10903
  };
9709
10904
  }
9710
10905
 
10906
+ // src/generators/checksum.ts
10907
+ function serializeForChecksum(document) {
10908
+ const { generatedAt: _generatedAt, ...content } = document;
10909
+ return JSON.stringify(content, null, 2);
10910
+ }
10911
+
9711
10912
  // src/pipeline/enrichment.ts
9712
10913
  function enrichScreenForAgent(screen) {
9713
10914
  const targets = mergeTargets([
@@ -9784,7 +10985,10 @@ function mergeTargets(targets) {
9784
10985
  for (const target of targets) {
9785
10986
  if (!target.id) continue;
9786
10987
  const existing = byId.get(target.id);
9787
- byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
10988
+ byId.set(
10989
+ target.id,
10990
+ existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
10991
+ );
9788
10992
  }
9789
10993
  return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
9790
10994
  }
@@ -9829,7 +11033,10 @@ function synthesizeFlows(screen, targets) {
9829
11033
  intent: "destructive_action",
9830
11034
  steps: [
9831
11035
  { type: "press", target: action.id, label: action.label },
9832
- { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
11036
+ {
11037
+ type: "confirm",
11038
+ description: "Wait for native or custom confirmation before continuing"
11039
+ },
9833
11040
  { type: "wait", description: describeWait(action) }
9834
11041
  ],
9835
11042
  waitPolicy: waitPolicyForAction(action),
@@ -9844,10 +11051,19 @@ function synthesizeFlows(screen, targets) {
9844
11051
  title: `Act on an item in ${collection.id}`,
9845
11052
  intent: "list_action",
9846
11053
  steps: [
9847
- { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
9848
- { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
11054
+ {
11055
+ type: "choose-list-item",
11056
+ target: collection.id,
11057
+ description: "Resolve the user reference to a visible or searchable row"
11058
+ },
11059
+ {
11060
+ type: "press",
11061
+ description: collection.rowAction?.description ?? "Open the row action"
11062
+ }
9849
11063
  ],
9850
- waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
11064
+ waitPolicy: {
11065
+ expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
11066
+ }
9851
11067
  });
9852
11068
  }
9853
11069
  }
@@ -9855,25 +11071,32 @@ function synthesizeFlows(screen, targets) {
9855
11071
  }
9856
11072
  function waitPolicyForAction(action) {
9857
11073
  const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
9858
- const signals = [action.successSignal, action.failureSignal].filter(Boolean);
11074
+ const signals = [action.successSignal, action.failureSignal].filter(
11075
+ Boolean
11076
+ );
11077
+ const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
9859
11078
  return {
9860
11079
  ...expectedOutcome ? { expectedOutcome } : {},
9861
11080
  ...signals && signals.length > 0 ? { signals } : {},
9862
- ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
11081
+ ...maxMs !== void 0 ? { maxMs } : {}
9863
11082
  };
9864
11083
  }
9865
11084
  function describeWait(action) {
9866
11085
  if (action.successSignal?.description) return action.successSignal.description;
9867
- if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
11086
+ if (action.successSignal?.type === "goBack")
11087
+ return "Wait for the app to return to the previous screen";
9868
11088
  if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
9869
- if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
11089
+ if (action.appilotsInferred?.expectedOutcome)
11090
+ return `Wait for ${action.appilotsInferred.expectedOutcome}`;
9870
11091
  return "Wait for the UI to settle";
9871
11092
  }
9872
11093
  function synthesizeAgentHints(screen, targets, flows) {
9873
11094
  const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
9874
11095
  const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
9875
11096
  const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
9876
- const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
11097
+ const firstAsyncAction = screen.actions.find(
11098
+ (action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
11099
+ );
9877
11100
  const hints = {
9878
11101
  primaryGoal: synthesizePrimaryGoal(screen),
9879
11102
  commonTasks,
@@ -9900,7 +11123,9 @@ function synthesizePrimaryGoal(screen) {
9900
11123
  }
9901
11124
  const fieldCount = uniqueFields.size;
9902
11125
  if (fieldCount > 0) {
9903
- const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
11126
+ const requiredCount = Array.from(uniqueFields.values()).filter(
11127
+ (field) => field.required
11128
+ ).length;
9904
11129
  const fieldLabel = fieldCount === 1 ? "field" : "fields";
9905
11130
  goals.push(
9906
11131
  requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
@@ -9914,7 +11139,9 @@ function synthesizePrimaryGoal(screen) {
9914
11139
  if (submitCount > 0) {
9915
11140
  goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
9916
11141
  }
9917
- const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
11142
+ const asyncCount = screen.actions.filter(
11143
+ (action) => action.appilotsInferred?.isAsyncTrigger
11144
+ ).length;
9918
11145
  if (asyncCount > 0) {
9919
11146
  goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
9920
11147
  }
@@ -9989,7 +11216,7 @@ var MCPGenerator = class _MCPGenerator {
9989
11216
  const agentReadyScreens = mergedScreens.map(
9990
11217
  (screen) => enrichScreenForAgent({
9991
11218
  ...screen,
9992
- filePath: screen.filePath ? path8__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
11219
+ filePath: screen.filePath ? path2__namespace.default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
9993
11220
  })
9994
11221
  );
9995
11222
  const projectInfo = await this.getProjectInfo();
@@ -10013,11 +11240,11 @@ var MCPGenerator = class _MCPGenerator {
10013
11240
  }
10014
11241
  };
10015
11242
  const serialized = JSON.stringify(document, null, 2);
10016
- const checksum = this.calculateChecksum(serialized);
10017
- const filePath = path8__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
11243
+ const checksum = this.calculateChecksum(serializeForChecksum(document));
11244
+ const filePath = path2__namespace.default.resolve(outputDir, `mcp-document.${this.options.format}`);
10018
11245
  await fs.writeFile(filePath, serialized, "utf-8");
10019
11246
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10020
- const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
11247
+ const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
10021
11248
  await fs.writeFile(checksumFilePath, checksum, "utf-8");
10022
11249
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
10023
11250
  console.log("[MCPGenerator] Generation complete!");
@@ -10025,7 +11252,8 @@ var MCPGenerator = class _MCPGenerator {
10025
11252
  document,
10026
11253
  filePath,
10027
11254
  format: this.options.format,
10028
- checksum
11255
+ checksum,
11256
+ ...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
10029
11257
  };
10030
11258
  }
10031
11259
  /**
@@ -10037,7 +11265,7 @@ var MCPGenerator = class _MCPGenerator {
10037
11265
  * after calling `generate()`.
10038
11266
  */
10039
11267
  static async readPreviousChecksum(outputDir) {
10040
- const checksumFilePath = path8__namespace.default.resolve(outputDir, ".appilots-checksum");
11268
+ const checksumFilePath = path2__namespace.default.resolve(outputDir, ".appilots-checksum");
10041
11269
  try {
10042
11270
  const content = await fs.readFile(checksumFilePath, "utf-8");
10043
11271
  return content.trim() || null;
@@ -10056,7 +11284,7 @@ var MCPGenerator = class _MCPGenerator {
10056
11284
  */
10057
11285
  async getProjectInfo() {
10058
11286
  try {
10059
- const packageJsonPath = path8__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
11287
+ const packageJsonPath = path2__namespace.default.resolve(this.analyzerConfig.rootDir, "package.json");
10060
11288
  const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
10061
11289
  const packageJson = JSON.parse(packageJsonContent);
10062
11290
  return {
@@ -10139,12 +11367,12 @@ function getEnvOverrides(env = process.env) {
10139
11367
  return trimmed ? trimmed : void 0;
10140
11368
  };
10141
11369
  return {
10142
- apiKey: clean(env.APPILOTS_API_KEY),
11370
+ apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
10143
11371
  projectId: clean(env.APPILOTS_PROJECT_ID),
10144
11372
  serverUrl: clean(env.APPILOTS_SERVER_URL)
10145
11373
  };
10146
11374
  }
10147
- function loadConfig(onWarn) {
11375
+ function loadConfig(onWarn, options = {}) {
10148
11376
  const configPath = getConfigPath();
10149
11377
  const env = getEnvOverrides();
10150
11378
  let fileConfig = {};
@@ -10159,6 +11387,18 @@ function loadConfig(onWarn) {
10159
11387
  } else if (!env.apiKey) {
10160
11388
  return null;
10161
11389
  }
11390
+ const legacyWarnings = [];
11391
+ if (fileConfig.serverUrl === void 0) {
11392
+ for (const [alias, canonical] of Object.entries(KEY_ALIASES)) {
11393
+ if (canonical !== "serverUrl" || fileConfig[alias] === void 0) continue;
11394
+ fileConfig.serverUrl = fileConfig[alias];
11395
+ delete fileConfig[alias];
11396
+ legacyWarnings.push(
11397
+ `legacy key "${alias}" \u2014 using it as "serverUrl"; rename it in .appilotsrc`
11398
+ );
11399
+ break;
11400
+ }
11401
+ }
10162
11402
  const merged = {
10163
11403
  serverUrl: DEFAULT_SERVER_URL,
10164
11404
  outputDir: ".appilots",
@@ -10170,6 +11410,24 @@ function loadConfig(onWarn) {
10170
11410
  ...env.serverUrl ? { serverUrl: env.serverUrl } : {}
10171
11411
  };
10172
11412
  const validation = validateConfig(merged);
11413
+ validation.warnings.unshift(...legacyWarnings);
11414
+ if (options.requireApiKey === false) {
11415
+ validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
11416
+ validation.valid = validation.errors.length === 0;
11417
+ }
11418
+ if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
11419
+ for (const key of Object.keys(fileConfig)) {
11420
+ if (KNOWN_CONFIG_KEYS.includes(key)) continue;
11421
+ if (suggestConfigKey(key) !== "serverUrl") continue;
11422
+ validation.warnings = validation.warnings.filter(
11423
+ (warning) => !warning.startsWith(`unknown key "${key}"`)
11424
+ );
11425
+ validation.errors.push(
11426
+ `unknown key "${key}" looks like "serverUrl" \u2014 not falling back to ${DEFAULT_SERVER_URL}; rename the key to "serverUrl"`
11427
+ );
11428
+ validation.valid = false;
11429
+ }
11430
+ }
10173
11431
  if (onWarn) {
10174
11432
  const source = configPath ?? "environment variables";
10175
11433
  for (const warning of validation.warnings) {
@@ -10186,7 +11444,7 @@ function loadConfig(onWarn) {
10186
11444
  return merged;
10187
11445
  }
10188
11446
  function saveConfig(dir, config) {
10189
- const configPath = path8.join(dir, ".appilotsrc");
11447
+ const configPath = path2.join(dir, ".appilotsrc");
10190
11448
  const existingConfig = getConfigPath() ? loadConfig() : null;
10191
11449
  const mergedConfig = {
10192
11450
  apiKey: config.apiKey || existingConfig?.apiKey || "",
@@ -10213,19 +11471,19 @@ function saveConfig(dir, config) {
10213
11471
  }
10214
11472
  }
10215
11473
  function getConfigPath() {
10216
- let currentDir = path8.resolve(process.cwd());
10217
- const root = path8.resolve("/");
11474
+ let currentDir = path2.resolve(process.cwd());
11475
+ const root = path2.resolve("/");
10218
11476
  while (currentDir !== root) {
10219
- const configPath = path8.join(currentDir, ".appilotsrc");
11477
+ const configPath = path2.join(currentDir, ".appilotsrc");
10220
11478
  try {
10221
11479
  if (fs$1.existsSync(configPath) && fs$1.statSync(configPath).isFile()) {
10222
11480
  return configPath;
10223
11481
  }
10224
11482
  } catch {
10225
11483
  }
10226
- currentDir = path8.resolve(currentDir, "..");
11484
+ currentDir = path2.resolve(currentDir, "..");
10227
11485
  }
10228
- const rootConfigPath = path8.join(root, ".appilotsrc");
11486
+ const rootConfigPath = path2.join(root, ".appilotsrc");
10229
11487
  try {
10230
11488
  if (fs$1.existsSync(rootConfigPath) && fs$1.statSync(rootConfigPath).isFile()) {
10231
11489
  return rootConfigPath;
@@ -10589,7 +11847,7 @@ var AppilotsAPIClient = class {
10589
11847
  };
10590
11848
 
10591
11849
  // src/version.ts
10592
- var CLI_VERSION = "0.10.0";
11850
+ var CLI_VERSION = "0.11.3";
10593
11851
 
10594
11852
  exports.AppilotsAPIClient = AppilotsAPIClient;
10595
11853
  exports.CLI_VERSION = CLI_VERSION;