@appilots/cli 0.11.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.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import fs, { readFile, mkdir, writeFile } from 'fs/promises';
2
- import * as path8 from 'path';
3
- import path8__default, { join, resolve } from 'path';
2
+ import * as path2 from 'path';
3
+ import path2__default, { join, resolve } from 'path';
4
4
  import traverse4 from '@babel/traverse';
5
5
  import * as BabelTypes from '@babel/types';
6
- import glob from 'fast-glob';
6
+ import fastGlob from 'fast-glob';
7
7
  import * as parser from '@babel/parser';
8
8
  import { parse } from '@babel/parser';
9
- import { promises, readFileSync, writeFileSync, existsSync, statSync } from 'fs';
9
+ import { promises, readFileSync, writeFileSync, existsSync, statSync, readdirSync } from 'fs';
10
10
  import { createHash } from 'crypto';
11
11
 
12
12
  var __defProp = Object.defineProperty;
@@ -14,6 +14,13 @@ var __export = (target, all) => {
14
14
  for (var name in all)
15
15
  __defProp(target, name, { get: all[name], enumerable: true });
16
16
  };
17
+ function byCodeUnit(a, b) {
18
+ return a < b ? -1 : a > b ? 1 : 0;
19
+ }
20
+ async function globSorted(patterns, options) {
21
+ const files = await fastGlob(patterns, options);
22
+ return files.sort(byCodeUnit);
23
+ }
17
24
  var DEFAULT_PARSER_PLUGINS = [
18
25
  "jsx",
19
26
  "typescript",
@@ -398,14 +405,25 @@ var ScreenAnalyzer = class {
398
405
  strictScreens;
399
406
  /** Glob patterns that identify screen files in strict mode */
400
407
  screenPatterns;
408
+ /**
409
+ * Arquivos que uma ROTA monta (`component={…}` resolvido pelo
410
+ * `NavigationAnalyzer`). Passam pelo filtro estrito sem depender de
411
+ * convenção de nome, porque um componente que uma rota monta É uma tela por
412
+ * definição — não é heurística, é o que o app declarou.
413
+ *
414
+ * É isto que resolve o caso `rocketchat`, cujas telas se chamam `*View.tsx`
415
+ * em `app/views/`, e o `coopcycle`, que não tem diretório `screens/` nenhum.
416
+ */
417
+ routeTargetFiles;
401
418
  /** §D: Count of screens filtered out in strict mode (available after analyze()) */
402
419
  screensFilteredOut = 0;
403
420
  constructor(config, options) {
404
421
  this.config = config;
422
+ this.routeTargetFiles = options?.routeTargetFiles ?? /* @__PURE__ */ new Set();
405
423
  this.strictScreens = options?.strictScreens ?? false;
406
424
  this.screenPatterns = options?.screenPatterns ?? [
407
- "**/*Screen.{ts,tsx}",
408
- "**/screens/**/*.{ts,tsx}"
425
+ "**/*Screen.{ts,tsx,js,jsx}",
426
+ "**/screens/**/*.{ts,tsx,js,jsx}"
409
427
  ];
410
428
  }
411
429
  /** Analyze all screens in the project */
@@ -417,7 +435,7 @@ var ScreenAnalyzer = class {
417
435
  console.log(`[ScreenAnalyzer] Strict mode ON \u2014 screen patterns:`, this.screenPatterns);
418
436
  }
419
437
  }
420
- const files = await glob(include, {
438
+ const files = await globSorted(include, {
421
439
  cwd: this.config.rootDir,
422
440
  ignore: exclude
423
441
  });
@@ -426,23 +444,24 @@ var ScreenAnalyzer = class {
426
444
  }
427
445
  let screenPatternFiles = null;
428
446
  if (this.strictScreens) {
429
- const matched = await glob(this.screenPatterns, {
447
+ const matched = await globSorted(this.screenPatterns, {
430
448
  cwd: this.config.rootDir,
431
449
  ignore: exclude
432
450
  });
433
- screenPatternFiles = new Set(matched.map((f) => path8__default.resolve(this.config.rootDir, f)));
451
+ screenPatternFiles = new Set(matched.map((f) => path2__default.resolve(this.config.rootDir, f)));
434
452
  }
435
453
  const screens = [];
436
454
  this.screensFilteredOut = 0;
437
455
  for (const file of files) {
438
- const filePath = path8__default.resolve(this.config.rootDir, file);
456
+ const filePath = path2__default.resolve(this.config.rootDir, file);
439
457
  try {
440
458
  const descriptor = await this.analyzeFile(filePath);
441
459
  if (!descriptor) continue;
442
460
  if (this.strictScreens) {
443
461
  const hasRegisterScreen = descriptor.__hasRegisterScreen === true;
444
462
  const matchesPattern = screenPatternFiles?.has(filePath) ?? false;
445
- if (!hasRegisterScreen && !matchesPattern) {
463
+ const isRouteTarget = this.routeTargetFiles.has(filePath);
464
+ if (!hasRegisterScreen && !matchesPattern && !isRouteTarget) {
446
465
  this.screensFilteredOut++;
447
466
  if (this.verbose) {
448
467
  console.log(`[ScreenAnalyzer] \u2717 Filtered (strict): ${file}`);
@@ -650,6 +669,8 @@ var ScreenAnalyzer = class {
650
669
  action.riskLevel = value.value;
651
670
  } else if (key === "nativeConfirmationExpected" && BabelTypes.isBooleanLiteral(value)) {
652
671
  action.nativeConfirmationExpected = value.value;
672
+ } else if (key === "asyncBudgetMs" && BabelTypes.isNumericLiteral(value) && Number.isInteger(value.value) && value.value > 0) {
673
+ action.asyncBudgetMs = value.value;
653
674
  } else if (key === "appilotsInferred" && BabelTypes.isObjectExpression(value)) {
654
675
  action.appilotsInferred = this.parseAppilotsInferredObject(value);
655
676
  }
@@ -1629,43 +1650,871 @@ var ScreenAnalyzer = class {
1629
1650
  * E.g., /src/screens/ItemListScreen.tsx -> ItemListScreen
1630
1651
  */
1631
1652
  extractScreenName(filePath) {
1632
- const basename2 = path8__default.basename(filePath);
1653
+ const basename2 = path2__default.basename(filePath);
1633
1654
  return basename2.replace(/\.(tsx?|jsx?)$/, "");
1634
1655
  }
1635
1656
  };
1657
+ var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
1658
+ var MAX_HOPS = 8;
1659
+ var ModuleGraph = class {
1660
+ asts = /* @__PURE__ */ new Map();
1661
+ resolved = /* @__PURE__ */ new Map();
1662
+ /** `@src/*` → `<root>/src/*`, lido do tsconfig do app. */
1663
+ aliases;
1664
+ /** `uniswap` → `<repo>/packages/uniswap`, lido do workspace do monorepo. */
1665
+ workspacePackages;
1666
+ constructor(rootDir) {
1667
+ this.aliases = rootDir ? readTsconfigAliases(rootDir) : [];
1668
+ this.workspacePackages = rootDir ? readWorkspacePackages(rootDir) : [];
1669
+ }
1670
+ /** AST de um arquivo, memoizada. `null` quando não parseia. */
1671
+ parse(file) {
1672
+ const cached = this.asts.get(file);
1673
+ if (cached !== void 0) return cached;
1674
+ let ast = null;
1675
+ try {
1676
+ ast = parser.parse(readFileSync(file, "utf-8"), {
1677
+ sourceType: "module",
1678
+ plugins: ["jsx", "typescript"]
1679
+ });
1680
+ } catch {
1681
+ ast = null;
1682
+ }
1683
+ this.asts.set(file, ast);
1684
+ return ast;
1685
+ }
1686
+ /**
1687
+ * `./account/Home` a partir de `src/navigation/index.tsx` → caminho absoluto.
1688
+ * Só resolve caminho relativo: import de pacote (`@react-navigation/native`)
1689
+ * é de terceiro e não tem tela nossa dentro.
1690
+ */
1691
+ resolve(fromFile, spec) {
1692
+ const key = `${fromFile} ${spec}`;
1693
+ const cached = this.resolved.get(key);
1694
+ if (cached !== void 0) return cached;
1695
+ let base = null;
1696
+ if (spec.startsWith(".")) {
1697
+ base = path2__default.resolve(path2__default.dirname(fromFile), spec);
1698
+ } else {
1699
+ for (const { prefix, target } of this.aliases) {
1700
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
1701
+ base = path2__default.join(target, spec.slice(prefix.length));
1702
+ break;
1703
+ }
1704
+ }
1705
+ if (!base) {
1706
+ for (const pkg of this.workspacePackages) {
1707
+ if (spec === pkg.name || spec.startsWith(pkg.name + "/")) {
1708
+ base = path2__default.join(pkg.dir, spec.slice(pkg.name.length));
1709
+ break;
1710
+ }
1711
+ }
1712
+ }
1713
+ }
1714
+ if (!base) {
1715
+ this.resolved.set(key, null);
1716
+ return null;
1717
+ }
1718
+ const candidates = [
1719
+ base,
1720
+ ...EXTENSIONS.map((e) => base + e),
1721
+ ...EXTENSIONS.map((e) => path2__default.join(base, "index" + e))
1722
+ ];
1723
+ let found = null;
1724
+ for (const c of candidates) {
1725
+ try {
1726
+ if (existsSync(c) && statSync(c).isFile()) {
1727
+ found = c;
1728
+ break;
1729
+ }
1730
+ } catch {
1731
+ }
1732
+ }
1733
+ this.resolved.set(key, found);
1734
+ return found;
1735
+ }
1736
+ /** `import`s do arquivo, por nome local. */
1737
+ imports(file) {
1738
+ const out = /* @__PURE__ */ new Map();
1739
+ const ast = this.parse(file);
1740
+ if (!ast) return out;
1741
+ for (const stmt of ast.program.body) {
1742
+ if (!BabelTypes.isImportDeclaration(stmt)) continue;
1743
+ const source = stmt.source.value;
1744
+ for (const spec of stmt.specifiers) {
1745
+ if (BabelTypes.isImportDefaultSpecifier(spec)) {
1746
+ out.set(spec.local.name, { source, imported: "default" });
1747
+ } else if (BabelTypes.isImportNamespaceSpecifier(spec)) {
1748
+ out.set(spec.local.name, { source, imported: "*" });
1749
+ } else if (BabelTypes.isImportSpecifier(spec)) {
1750
+ const imported = BabelTypes.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value;
1751
+ out.set(spec.local.name, { source, imported });
1752
+ }
1753
+ }
1754
+ }
1755
+ return out;
1756
+ }
1757
+ /** `const X = <init>` no topo do arquivo, incluindo `export const`. */
1758
+ topLevelInit(file, name) {
1759
+ const ast = this.parse(file);
1760
+ if (!ast) return null;
1761
+ for (const stmt of ast.program.body) {
1762
+ const decl = BabelTypes.isExportNamedDeclaration(stmt) ? stmt.declaration : stmt;
1763
+ if (!BabelTypes.isVariableDeclaration(decl)) continue;
1764
+ for (const d of decl.declarations) {
1765
+ if (BabelTypes.isIdentifier(d.id) && d.id.name === name && d.init) {
1766
+ return BabelTypes.isTSAsExpression(d.init) ? d.init.expression : d.init;
1767
+ }
1768
+ }
1769
+ }
1770
+ return null;
1771
+ }
1772
+ /**
1773
+ * Onde `name` é DEFINIDO — segue import e reexport de barrel.
1774
+ * Devolve o arquivo e o nome sob o qual ele é definido lá.
1775
+ */
1776
+ resolveBinding(file, name, hops = 0) {
1777
+ if (hops > MAX_HOPS) return null;
1778
+ if (this.topLevelInit(file, name) !== null) return { file, name };
1779
+ const binding = this.imports(file).get(name);
1780
+ if (binding) {
1781
+ const target = this.resolve(file, binding.source);
1782
+ if (!target) return null;
1783
+ const next = binding.imported === "default" || binding.imported === "*" ? name : binding.imported;
1784
+ const deeper = this.resolveBinding(target, next, hops + 1);
1785
+ return deeper ?? { file: target, name: next };
1786
+ }
1787
+ const ast = this.parse(file);
1788
+ if (ast) {
1789
+ for (const stmt of ast.program.body) {
1790
+ if (!BabelTypes.isExportNamedDeclaration(stmt) || !stmt.source) continue;
1791
+ for (const spec of stmt.specifiers) {
1792
+ if (!BabelTypes.isExportSpecifier(spec)) continue;
1793
+ const exported = BabelTypes.isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
1794
+ if (exported !== name) continue;
1795
+ const target = this.resolve(file, stmt.source.value);
1796
+ if (!target) return null;
1797
+ const local = spec.local.name;
1798
+ return this.resolveBinding(target, local, hops + 1) ?? { file: target, name: local };
1799
+ }
1800
+ }
1801
+ }
1802
+ if (ast) {
1803
+ for (const stmt of ast.program.body) {
1804
+ if (!BabelTypes.isExportAllDeclaration(stmt)) continue;
1805
+ const target = this.resolve(file, stmt.source.value);
1806
+ if (!target || target === file) continue;
1807
+ const deeper = this.resolveBinding(target, name, hops + 1);
1808
+ if (deeper) return deeper;
1809
+ }
1810
+ }
1811
+ if (ast) {
1812
+ for (const stmt of ast.program.body) {
1813
+ if (!BabelTypes.isExportDefaultDeclaration(stmt)) continue;
1814
+ if (BabelTypes.isIdentifier(stmt.declaration)) {
1815
+ const local = stmt.declaration.name;
1816
+ if (local === name) return null;
1817
+ return this.resolveBinding(file, local, hops + 1) ?? { file, name: local };
1818
+ }
1819
+ }
1820
+ }
1821
+ return null;
1822
+ }
1823
+ /**
1824
+ * O valor string de uma expressão de nome de rota, ou `null`.
1825
+ *
1826
+ * Cobre `"Chat"`, `ROUTES.CHAT`, `ROUTES.ONBOARDING.SPLASH` e `SOME_CONST` —
1827
+ * seguindo import quando o objeto vem de outro arquivo. NÃO cobre template
1828
+ * com interpolação nem valor calculado, de propósito.
1829
+ */
1830
+ stringConstant(file, node, hops = 0) {
1831
+ if (!node || hops > MAX_HOPS) return null;
1832
+ if (BabelTypes.isStringLiteral(node)) return node.value;
1833
+ if (BabelTypes.isTemplateLiteral(node)) {
1834
+ return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? null : null;
1835
+ }
1836
+ if (BabelTypes.isTSAsExpression(node)) return this.stringConstant(file, node.expression, hops + 1);
1837
+ const chain = memberChain(node);
1838
+ if (!chain) return null;
1839
+ const origin = this.resolveBinding(file, chain.root);
1840
+ if (!origin) return null;
1841
+ let current = this.topLevelInit(origin.file, origin.name);
1842
+ if (!current) return null;
1843
+ for (const key of chain.path) {
1844
+ if (!BabelTypes.isObjectExpression(current)) return null;
1845
+ const prop = objectProperty(current, key);
1846
+ if (!prop) return null;
1847
+ current = BabelTypes.isTSAsExpression(prop) ? prop.expression : prop;
1848
+ }
1849
+ return BabelTypes.isStringLiteral(current) ? current.value : null;
1850
+ }
1851
+ /**
1852
+ * O ARQUIVO onde vive o componente de uma rota, ou `null`.
1853
+ *
1854
+ * Aceita as três formas que o corpus mostrou: identificador
1855
+ * (`component={Home}`), membro de barrel (`component={screens.AccountHome}`)
1856
+ * e componente embrulhado em HOC (`component={gestureHandlerRootHOC(Chat)}`,
1857
+ * que é como o `pocketpal` monta todas as telas do Drawer).
1858
+ */
1859
+ componentFile(file, node, hops = 0) {
1860
+ if (!node || hops > MAX_HOPS) return null;
1861
+ if (BabelTypes.isCallExpression(node)) {
1862
+ for (const arg of node.arguments) {
1863
+ if (BabelTypes.isIdentifier(arg) || BabelTypes.isMemberExpression(arg)) {
1864
+ const inner = this.componentFile(file, arg, hops + 1);
1865
+ if (inner) return inner;
1866
+ }
1867
+ }
1868
+ return null;
1869
+ }
1870
+ if (BabelTypes.isTSAsExpression(node)) return this.componentFile(file, node.expression, hops + 1);
1871
+ const chain = memberChain(node);
1872
+ if (!chain) return null;
1873
+ const origin = this.resolveBinding(file, chain.root);
1874
+ if (!origin) return null;
1875
+ if (chain.path.length === 0) return origin.file;
1876
+ const init = this.topLevelInit(origin.file, origin.name);
1877
+ if (init && BabelTypes.isObjectExpression(init)) {
1878
+ const prop = objectProperty(init, chain.path[0]);
1879
+ if (prop) return this.componentFile(origin.file, prop, hops + 1);
1880
+ }
1881
+ const viaExport = this.resolveBinding(origin.file, chain.path[0], hops + 1);
1882
+ return viaExport?.file ?? null;
1883
+ }
1884
+ /**
1885
+ * Os arquivos DO PROJETO que este arquivo renderiza como JSX.
1886
+ *
1887
+ * `<ChatView …/>` em `ChatScreen.tsx` → `components/ChatView/ChatView.tsx`.
1888
+ * Import de pacote devolve `null` no `resolve` e fica de fora: componente de
1889
+ * terceiro não tem tela nossa dentro.
1890
+ *
1891
+ * Ignora `<X.Screen>` e `<X.Navigator>` de propósito — navegação é outro
1892
+ * extractor, e um navegador não é conteúdo de tela.
1893
+ */
1894
+ renderedComponentFiles(file) {
1895
+ const ast = this.parse(file);
1896
+ if (!ast) return [];
1897
+ const names = /* @__PURE__ */ new Set();
1898
+ const visit = (node) => {
1899
+ if (!node || typeof node !== "object") return;
1900
+ if (BabelTypes.isJSXOpeningElement(node) && BabelTypes.isJSXIdentifier(node.name)) {
1901
+ const n = node.name.name;
1902
+ if (/^[A-Z]/.test(n)) names.add(n);
1903
+ }
1904
+ for (const key of Object.keys(node)) {
1905
+ const value = node[key];
1906
+ if (Array.isArray(value)) {
1907
+ for (const item of value) visit(item);
1908
+ } else if (value && typeof value === "object" && "type" in value) {
1909
+ visit(value);
1910
+ }
1911
+ }
1912
+ };
1913
+ visit(ast.program);
1914
+ const out = /* @__PURE__ */ new Set();
1915
+ for (const name of names) {
1916
+ const origin = this.resolveBinding(file, name);
1917
+ if (origin && origin.file !== file) out.add(origin.file);
1918
+ }
1919
+ return [...out];
1920
+ }
1921
+ };
1922
+ function memberChain(node) {
1923
+ const chain = [];
1924
+ let current = node;
1925
+ while (BabelTypes.isMemberExpression(current)) {
1926
+ if (current.computed) {
1927
+ if (!BabelTypes.isStringLiteral(current.property)) return null;
1928
+ chain.unshift(current.property.value);
1929
+ } else if (BabelTypes.isIdentifier(current.property)) {
1930
+ chain.unshift(current.property.name);
1931
+ } else {
1932
+ return null;
1933
+ }
1934
+ current = current.object;
1935
+ }
1936
+ return BabelTypes.isIdentifier(current) ? { root: current.name, path: chain } : null;
1937
+ }
1938
+ function objectProperty(obj, key) {
1939
+ for (const prop of obj.properties) {
1940
+ if (!BabelTypes.isObjectProperty(prop)) continue;
1941
+ const name = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
1942
+ if (name === key && BabelTypes.isExpression(prop.value)) return prop.value;
1943
+ }
1944
+ return null;
1945
+ }
1946
+ function readTsconfigAliases(rootDir) {
1947
+ const file = path2__default.join(rootDir, "tsconfig.json");
1948
+ if (!existsSync(file)) return [];
1949
+ let parsed;
1950
+ try {
1951
+ const raw = stripJsonComments(readFileSync(file, "utf-8")).replace(/,(\s*[}\]])/g, "$1");
1952
+ parsed = JSON.parse(raw);
1953
+ } catch {
1954
+ return [];
1955
+ }
1956
+ const paths = parsed.compilerOptions?.paths;
1957
+ if (!paths) return [];
1958
+ const baseUrl = path2__default.resolve(rootDir, parsed.compilerOptions?.baseUrl ?? ".");
1959
+ const out = [];
1960
+ for (const [pattern, targets] of Object.entries(paths)) {
1961
+ const first = targets[0];
1962
+ if (typeof first !== "string") continue;
1963
+ out.push({
1964
+ prefix: pattern.replace(/\/?\*$/, ""),
1965
+ target: path2__default.resolve(baseUrl, first.replace(/\/?\*$/, ""))
1966
+ });
1967
+ }
1968
+ return out.sort((a, b) => b.prefix.length - a.prefix.length);
1969
+ }
1970
+ function stripJsonComments(text) {
1971
+ let out = "";
1972
+ let inString = false;
1973
+ let escaped = false;
1974
+ for (let i = 0; i < text.length; i++) {
1975
+ const c = text[i];
1976
+ if (inString) {
1977
+ out += c;
1978
+ if (escaped) escaped = false;
1979
+ else if (c === "\\") escaped = true;
1980
+ else if (c === '"') inString = false;
1981
+ continue;
1982
+ }
1983
+ if (c === '"') {
1984
+ inString = true;
1985
+ out += c;
1986
+ continue;
1987
+ }
1988
+ if (c === "/" && text[i + 1] === "/") {
1989
+ while (i < text.length && text[i] !== "\n") i++;
1990
+ out += "\n";
1991
+ continue;
1992
+ }
1993
+ if (c === "/" && text[i + 1] === "*") {
1994
+ i += 2;
1995
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
1996
+ i++;
1997
+ continue;
1998
+ }
1999
+ out += c;
2000
+ }
2001
+ return out;
2002
+ }
2003
+ function readWorkspacePackages(rootDir) {
2004
+ const start = path2__default.resolve(rootDir);
2005
+ let dir = start;
2006
+ for (let i = 0; i < 6; i++) {
2007
+ const globs = workspaceGlobs(dir);
2008
+ if (globs.length > 0) {
2009
+ const packages = expandWorkspaceGlobs(dir, globs);
2010
+ const containsApp = packages.some(
2011
+ (p) => start === p.dir || start.startsWith(p.dir + path2__default.sep)
2012
+ );
2013
+ return containsApp ? packages : [];
2014
+ }
2015
+ const parent = path2__default.dirname(dir);
2016
+ if (parent === dir) break;
2017
+ dir = parent;
2018
+ }
2019
+ return [];
2020
+ }
2021
+ function workspaceGlobs(dir) {
2022
+ const pnpm = path2__default.join(dir, "pnpm-workspace.yaml");
2023
+ if (existsSync(pnpm)) {
2024
+ try {
2025
+ const lines = readFileSync(pnpm, "utf-8").split(/\r?\n/);
2026
+ const out = [];
2027
+ let inPackages = false;
2028
+ for (const line of lines) {
2029
+ if (/^packages:/.test(line)) {
2030
+ inPackages = true;
2031
+ continue;
2032
+ }
2033
+ if (inPackages) {
2034
+ const m = /^\s*-\s*["']?([^"'#]+?)["']?\s*$/.exec(line);
2035
+ if (m) out.push(m[1].trim());
2036
+ else if (/^\S/.test(line)) break;
2037
+ }
2038
+ }
2039
+ if (out.length) return out;
2040
+ } catch {
2041
+ }
2042
+ }
2043
+ const pkgPath = path2__default.join(dir, "package.json");
2044
+ if (!existsSync(pkgPath)) return [];
2045
+ try {
2046
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
2047
+ const ws = pkg.workspaces;
2048
+ if (Array.isArray(ws)) return ws;
2049
+ if (ws && Array.isArray(ws.packages)) return ws.packages;
2050
+ } catch {
2051
+ }
2052
+ return [];
2053
+ }
2054
+ function expandWorkspaceGlobs(root, globs) {
2055
+ const out = [];
2056
+ const add = (dir) => {
2057
+ const pkgPath = path2__default.join(dir, "package.json");
2058
+ if (!existsSync(pkgPath)) return;
2059
+ try {
2060
+ const name = JSON.parse(readFileSync(pkgPath, "utf-8")).name;
2061
+ if (name) out.push({ name, dir });
2062
+ } catch {
2063
+ }
2064
+ };
2065
+ for (const glob of globs) {
2066
+ if (glob.endsWith("/*")) {
2067
+ const parent = path2__default.join(root, glob.slice(0, -2));
2068
+ let entries = [];
2069
+ try {
2070
+ entries = readdirSync(parent);
2071
+ } catch {
2072
+ continue;
2073
+ }
2074
+ for (const entry of entries) {
2075
+ const dir = path2__default.join(parent, entry);
2076
+ try {
2077
+ if (statSync(dir).isDirectory()) add(dir);
2078
+ } catch {
2079
+ }
2080
+ }
2081
+ } else if (!glob.includes("*")) {
2082
+ add(path2__default.join(root, glob));
2083
+ }
2084
+ }
2085
+ return out.sort((a, b) => b.name.length - a.name.length);
2086
+ }
2087
+ var ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
2088
+ var PLATFORM_SUFFIXES = [".ios", ".android", ".native", ".web"];
2089
+ var ROUTE_DIR_CANDIDATES = ["app", "src/app"];
2090
+ function baseName(file) {
2091
+ let name = file;
2092
+ for (const ext of ROUTE_EXTENSIONS) {
2093
+ if (name.endsWith(ext)) {
2094
+ name = name.slice(0, -ext.length);
2095
+ break;
2096
+ }
2097
+ }
2098
+ for (const suffix of PLATFORM_SUFFIXES) {
2099
+ if (name.endsWith(suffix)) return name.slice(0, -suffix.length);
2100
+ }
2101
+ return name;
2102
+ }
2103
+ function isRouteFile(file) {
2104
+ return ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext));
2105
+ }
2106
+ function isSpecial(base) {
2107
+ if (base === "_layout") return true;
2108
+ if (base.startsWith("+")) return base !== "+not-found";
2109
+ return base.startsWith("_");
2110
+ }
2111
+ function layoutType(file) {
2112
+ try {
2113
+ const src = readFileSync(file, "utf-8");
2114
+ if (/<(?:Native)?Tabs\b/.test(src)) return "tab";
2115
+ if (/<Drawer\b/.test(src)) return "drawer";
2116
+ } catch {
2117
+ }
2118
+ return "stack";
2119
+ }
2120
+ function findLayout(dir) {
2121
+ for (const ext of ROUTE_EXTENSIONS) {
2122
+ const candidate = path2__default.join(dir, "_layout" + ext);
2123
+ if (existsSync(candidate)) return candidate;
2124
+ }
2125
+ return null;
2126
+ }
2127
+ function analyzeExpoRouter(appRoot) {
2128
+ let routeDir = null;
2129
+ for (const candidate of ROUTE_DIR_CANDIDATES) {
2130
+ const full = path2__default.join(appRoot, candidate);
2131
+ if (existsSync(full) && findLayout(full)) {
2132
+ routeDir = full;
2133
+ break;
2134
+ }
2135
+ }
2136
+ if (!routeDir) return null;
2137
+ const routes = [];
2138
+ const navigators = /* @__PURE__ */ new Map();
2139
+ const seen = /* @__PURE__ */ new Set();
2140
+ const walk = (dir, segments, navigator) => {
2141
+ const layout = findLayout(dir);
2142
+ const current = layout ? segments.join("/") || "/" : navigator;
2143
+ if (layout && !navigators.has(current)) navigators.set(current, layoutType(layout));
2144
+ let entries;
2145
+ try {
2146
+ entries = readdirSync(dir);
2147
+ } catch {
2148
+ return;
2149
+ }
2150
+ for (const entry of entries.sort()) {
2151
+ const full = path2__default.join(dir, entry);
2152
+ let isDir = false;
2153
+ try {
2154
+ isDir = statSync(full).isDirectory();
2155
+ } catch {
2156
+ continue;
2157
+ }
2158
+ if (isDir) {
2159
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
2160
+ walk(full, [...segments, entry], current);
2161
+ continue;
2162
+ }
2163
+ if (!isRouteFile(entry)) continue;
2164
+ const base = baseName(entry);
2165
+ if (isSpecial(base)) continue;
2166
+ const routeSegments = base === "index" ? segments : [...segments, base];
2167
+ const name = "/" + routeSegments.join("/");
2168
+ if (seen.has(name)) continue;
2169
+ seen.add(name);
2170
+ routes.push({
2171
+ name,
2172
+ componentFile: full,
2173
+ navigatorName: current,
2174
+ navigatorType: navigators.get(current) ?? "stack",
2175
+ params: routeSegments.filter((s) => s.startsWith("[") && s.endsWith("]")).map((s) => s.slice(1, -1).replace(/^\.\.\./, ""))
2176
+ });
2177
+ }
2178
+ };
2179
+ walk(routeDir, [], "/");
2180
+ return {
2181
+ routeDir: path2__default.relative(appRoot, routeDir),
2182
+ routes,
2183
+ navigators: [...navigators.entries()].map(([name, type]) => ({ name, type }))
2184
+ };
2185
+ }
2186
+
2187
+ // src/analyzers/NavigationAnalyzer.ts
2188
+ var NAVIGATOR_FACTORIES = {
2189
+ createStackNavigator: "stack",
2190
+ createNativeStackNavigator: "stack",
2191
+ createBottomTabNavigator: "tab",
2192
+ createTabNavigator: "tab",
2193
+ createMaterialTopTabNavigator: "tab",
2194
+ createMaterialBottomTabNavigator: "tab",
2195
+ createDrawerNavigator: "drawer"
2196
+ };
2197
+ var CUSTOM_FACTORY = /^create[A-Za-z0-9]*(Navigator|Stack|Tabs?|Drawer)$/;
2198
+ function unwrapStaticScreen(value) {
2199
+ if (BabelTypes.isCallExpression(value)) {
2200
+ const arg = value.arguments[0];
2201
+ return arg ? unwrapStaticScreen(arg) : null;
2202
+ }
2203
+ if (BabelTypes.isObjectExpression(value)) {
2204
+ for (const prop of value.properties) {
2205
+ if (!BabelTypes.isObjectProperty(prop)) continue;
2206
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : null;
2207
+ if (key === "screen" && BabelTypes.isExpression(prop.value)) return prop.value;
2208
+ }
2209
+ return null;
2210
+ }
2211
+ return BabelTypes.isExpression(value) ? value : null;
2212
+ }
2213
+ function inferNavigatorType(factoryName) {
2214
+ if (/drawer/i.test(factoryName)) return "drawer";
2215
+ if (/tab/i.test(factoryName)) return "tab";
2216
+ return "stack";
2217
+ }
2218
+ var NAVIGATION_EVIDENCE = new RegExp(
2219
+ // `\.Navigator`/`\.Screen` entram porque o JSX pode estar num arquivo que
2220
+ // não menciona fábrica nenhuma: `comapeo` declara `RootStack` em
2221
+ // `Stack/RootStack.ts` e escreve todas as 120 rotas em `Stack/index.tsx`,
2222
+ // que não cita `createNativeStackNavigator` uma vez sequer. Sem isto a
2223
+ // junção entre arquivos nunca chega a ser tentada.
2224
+ // Sem `\\b` depois da palavra — `createNativeStackNavigatorWithAuth` não tem
2225
+ // fronteira ali, e exigi-la fecharia o portão para o arquivo que declara os
2226
+ // seis navegadores do `bluesky`.
2227
+ `create[A-Za-z0-9]*(?:Navigator|Stack|Tabs?|Drawer)|ParamList|\\.Navigator\\b|\\.Screen\\b`
2228
+ );
1636
2229
  var NavigationAnalyzer = class {
1637
2230
  config;
1638
2231
  /** Extra glob patterns for navigation file discovery (added to defaults) */
1639
2232
  navigationInclude;
1640
2233
  /** Extra glob patterns to exclude from navigation analysis */
1641
2234
  navigationExclude;
2235
+ /** Exposto para a composição reusar o cache de AST em vez de reparsear a árvore. */
2236
+ graph;
2237
+ /**
2238
+ * Rota → arquivo do componente que ela monta. É a única ligação entre o grafo
2239
+ * de navegação e a árvore de telas; sem ela as duas metades do documento
2240
+ * falam de coisas diferentes. Populada por `analyze()`.
2241
+ */
2242
+ routeTargets = /* @__PURE__ */ new Map();
2243
+ /**
2244
+ * Arquivos que DECLARAM uma fábrica de navegador.
2245
+ *
2246
+ * Um navegador aninhado é montado como `component=` de uma rota — no
2247
+ * `apps/example-app`, `<Stack.Screen name="Main" component={MainTabNavigator} />`.
2248
+ * Promover esse arquivo a tela criaria seis "telas" sem uma única ação, que é
2249
+ * exatamente o falso positivo que o corpus existe para não repetir. Um
2250
+ * navegador é um contêiner de rotas; a tela está um nível abaixo.
2251
+ *
2252
+ * Entram aqui os dois lados: o arquivo que CHAMA a fábrica e o arquivo que
2253
+ * RENDERIZA `<X.Navigator>`. Não são o mesmo — `bluewallet` declara
2254
+ * `DetailViewStack` num arquivo e escreve o JSX em
2255
+ * `navigation/DetailViewScreensStack.tsx`, e checar só o primeiro deixava o
2256
+ * segundo entrar como tela.
2257
+ */
2258
+ navigatorFiles = /* @__PURE__ */ new Set();
2259
+ /** Preenchido por `analyze()`. Vazio antes disso. */
2260
+ diagnostics = {
2261
+ filesScanned: 0,
2262
+ filesWithEvidence: 0,
2263
+ navigatorsDeclared: 0,
2264
+ navigatorsWithJsx: 0,
2265
+ routes: 0,
2266
+ routesLinkedToFile: 0
2267
+ };
1642
2268
  constructor(config, options) {
1643
2269
  this.config = config;
2270
+ this.graph = new ModuleGraph(config.rootDir);
1644
2271
  this.navigationInclude = options?.navigationInclude ?? [];
1645
2272
  this.navigationExclude = options?.navigationExclude ?? [];
1646
2273
  }
1647
2274
  /** Build the full navigation graph */
1648
2275
  async analyze() {
1649
2276
  const navigationFiles = await this.findNavigationFiles();
1650
- const parsedNavigators = [];
1651
2277
  const typeExports = [];
2278
+ this.routeTargets.clear();
2279
+ const declarations = /* @__PURE__ */ new Map();
2280
+ const evidenceFiles = [];
2281
+ this.navigatorFiles.clear();
1652
2282
  for (const filePath of navigationFiles) {
1653
2283
  try {
1654
2284
  const content = await promises.readFile(filePath, "utf-8");
1655
- const navigators = this.parseNavigators(content, filePath);
1656
- const types = this.parseParamTypes(content, filePath);
1657
- parsedNavigators.push(...navigators);
1658
- typeExports.push(...types);
2285
+ if (!NAVIGATION_EVIDENCE.test(content)) continue;
2286
+ evidenceFiles.push(filePath);
2287
+ for (const decl of this.collectDeclarations(filePath)) {
2288
+ declarations.set(`${decl.file}#${decl.name}`, decl);
2289
+ if (decl.confident) this.navigatorFiles.add(decl.file);
2290
+ }
2291
+ typeExports.push(...this.parseParamTypes(content, filePath));
1659
2292
  } catch (error) {
1660
2293
  console.warn(`Failed to parse ${filePath}:`, error);
1661
2294
  }
1662
2295
  }
2296
+ const parsedNavigators = [];
2297
+ for (const filePath of evidenceFiles) {
2298
+ try {
2299
+ parsedNavigators.push(...this.parseNavigatorUsages(filePath, declarations));
2300
+ } catch (error) {
2301
+ console.warn(`Failed to parse navigators in ${filePath}:`, error);
2302
+ }
2303
+ }
2304
+ let detachedScreens = 0;
2305
+ for (const filePath of evidenceFiles) {
2306
+ try {
2307
+ for (const detached of this.parseDetachedScreens(filePath, declarations)) {
2308
+ const existing = parsedNavigators.find(
2309
+ (nav) => nav.name === detached.name && nav.type === detached.type
2310
+ );
2311
+ if (!existing) {
2312
+ parsedNavigators.push(detached);
2313
+ detachedScreens += detached.screens.length;
2314
+ continue;
2315
+ }
2316
+ const seen = new Set(existing.screens.map((screen) => screen.name));
2317
+ for (const screen of detached.screens) {
2318
+ if (seen.has(screen.name)) continue;
2319
+ seen.add(screen.name);
2320
+ existing.screens.push(screen);
2321
+ detachedScreens++;
2322
+ }
2323
+ }
2324
+ } catch (error) {
2325
+ console.warn(`Failed to parse detached screens in ${filePath}:`, error);
2326
+ }
2327
+ }
2328
+ const expo = analyzeExpoRouter(this.config.rootDir);
2329
+ if (expo) {
2330
+ console.log(
2331
+ `[NavigationAnalyzer] expo-router em "${expo.routeDir}": ${expo.routes.length} rotas, ${expo.navigators.length} layouts`
2332
+ );
2333
+ this.diagnostics.fileBasedRouter = {
2334
+ kind: "expo-router",
2335
+ routeDir: expo.routeDir,
2336
+ routes: expo.routes.length
2337
+ };
2338
+ const byNavigator = /* @__PURE__ */ new Map();
2339
+ for (const nav of expo.navigators) {
2340
+ byNavigator.set(nav.name, { name: nav.name, type: nav.type, screens: [] });
2341
+ }
2342
+ for (const route of expo.routes) {
2343
+ const navigator = byNavigator.get(route.navigatorName) ?? byNavigator.set(route.navigatorName, {
2344
+ name: route.navigatorName,
2345
+ type: route.navigatorType,
2346
+ screens: []
2347
+ }).get(route.navigatorName);
2348
+ navigator.screens.push({
2349
+ name: route.name,
2350
+ navigatorName: route.navigatorName,
2351
+ navigatorType: route.navigatorType,
2352
+ componentFile: route.componentFile,
2353
+ ...route.params.length ? {
2354
+ params: route.params.map((name) => ({ name, type: "string", required: true }))
2355
+ } : {}
2356
+ });
2357
+ this.routeTargets.set(route.name, route.componentFile);
2358
+ }
2359
+ parsedNavigators.push(...byNavigator.values());
2360
+ }
2361
+ let staticOnly = 0;
2362
+ for (const decl of declarations.values()) {
2363
+ if (!decl.confident || !decl.staticScreens?.length) continue;
2364
+ const existing = parsedNavigators.find((n) => n.name === decl.name && n.type === decl.type);
2365
+ if (existing) {
2366
+ const seen = new Set(existing.screens.map((s) => s.name));
2367
+ for (const screen of decl.staticScreens) {
2368
+ if (!seen.has(screen.name)) existing.screens.push(screen);
2369
+ }
2370
+ continue;
2371
+ }
2372
+ parsedNavigators.push({ name: decl.name, type: decl.type, screens: decl.staticScreens });
2373
+ staticOnly++;
2374
+ }
2375
+ const confidentDeclarations = [...declarations.values()].filter((d) => d.confident).length;
2376
+ const uniqueRoutes = new Set(
2377
+ parsedNavigators.flatMap((nav) => nav.screens.map((screen) => screen.name))
2378
+ );
2379
+ this.diagnostics = {
2380
+ ...this.diagnostics,
2381
+ filesScanned: navigationFiles.length,
2382
+ filesWithEvidence: evidenceFiles.length,
2383
+ navigatorsDeclared: confidentDeclarations,
2384
+ navigatorsWithJsx: parsedNavigators.length,
2385
+ routes: uniqueRoutes.size,
2386
+ // `routeTargets` é indexado por nome de rota, e uma rota pode ter sido
2387
+ // registrada por um navegador que não sobreviveu. O mínimo evita a
2388
+ // cobertura acima de 100% que a medição do corpus expôs no `coopcycle`.
2389
+ routesLinkedToFile: Math.min(this.routeTargets.size, uniqueRoutes.size)
2390
+ };
2391
+ console.log(
2392
+ `[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`
2393
+ );
1663
2394
  this.attachParamsToNavigators(parsedNavigators, typeExports);
1664
2395
  return this.buildNavigationGraph(parsedNavigators);
1665
2396
  }
1666
- /** Find all navigation-related files */
2397
+ /** Fase 1: as fábricas `create*Navigator()` atribuídas a um nome neste arquivo. */
2398
+ collectDeclarations(filePath) {
2399
+ const ast = this.graph.parse(filePath);
2400
+ if (!ast) return [];
2401
+ const out = /* @__PURE__ */ new Map();
2402
+ traverse4(ast, {
2403
+ CallExpression: (nodePath) => {
2404
+ const callee = nodePath.node.callee;
2405
+ if (!BabelTypes.isIdentifier(callee)) return;
2406
+ const known = NAVIGATOR_FACTORIES[callee.name] ?? (CUSTOM_FACTORY.test(callee.name) ? inferNavigatorType(callee.name) : null);
2407
+ const type = known ?? inferNavigatorType(callee.name);
2408
+ const confident = known !== null;
2409
+ let up = nodePath.parentPath;
2410
+ for (let i = 0; i < 4 && up; i++) {
2411
+ if (BabelTypes.isVariableDeclarator(up.node)) break;
2412
+ if (!BabelTypes.isMemberExpression(up.node) && !BabelTypes.isCallExpression(up.node)) {
2413
+ up = null;
2414
+ break;
2415
+ }
2416
+ up = up.parentPath;
2417
+ }
2418
+ const declarator = up?.node;
2419
+ if (!declarator || !BabelTypes.isVariableDeclarator(declarator) || !BabelTypes.isIdentifier(declarator.id)) {
2420
+ return;
2421
+ }
2422
+ const name = declarator.id.name;
2423
+ if (out.get(name)?.confident) return;
2424
+ const staticScreens = confident ? this.parseStaticScreens(filePath, nodePath.node.arguments[0], name, type) : [];
2425
+ out.set(name, {
2426
+ file: filePath,
2427
+ name,
2428
+ type,
2429
+ confident,
2430
+ ...staticScreens.length ? { staticScreens } : {}
2431
+ });
2432
+ }
2433
+ });
2434
+ return [...out.values()];
2435
+ }
2436
+ /**
2437
+ * As rotas de `createXNavigator({ screens: { … } })`.
2438
+ *
2439
+ * Quatro formas no corpus, todas em `rocketchat`:
2440
+ * `NewServerView` — shorthand, nome = componente
2441
+ * `LoginView: createNativeStackScreen({ screen })` — embrulho da própria lib
2442
+ * `SelectListView: SelectListViewScreen` — identificador direto
2443
+ * `ChatsStackNavigator: ChatsStack` — navegador aninhado
2444
+ *
2445
+ * `groups: { G: { screens: { … } } }` também entra: faz parte da API estática
2446
+ * e agrupar rotas não muda o que elas são.
2447
+ */
2448
+ parseStaticScreens(filePath, config, navigatorName, navigatorType, depth = 0) {
2449
+ if (!config || !BabelTypes.isObjectExpression(config) || depth > 4) return [];
2450
+ const out = [];
2451
+ for (const prop of config.properties) {
2452
+ if (!BabelTypes.isObjectProperty(prop)) continue;
2453
+ const key = BabelTypes.isIdentifier(prop.key) ? prop.key.name : BabelTypes.isStringLiteral(prop.key) ? prop.key.value : null;
2454
+ if (!key) continue;
2455
+ if (key === "groups" && BabelTypes.isObjectExpression(prop.value)) {
2456
+ for (const group of prop.value.properties) {
2457
+ if (!BabelTypes.isObjectProperty(group) || !BabelTypes.isExpression(group.value)) continue;
2458
+ out.push(
2459
+ ...this.parseStaticScreens(
2460
+ filePath,
2461
+ group.value,
2462
+ navigatorName,
2463
+ navigatorType,
2464
+ depth + 1
2465
+ )
2466
+ );
2467
+ }
2468
+ continue;
2469
+ }
2470
+ if (key !== "screens" || !BabelTypes.isObjectExpression(prop.value)) continue;
2471
+ for (const entry of prop.value.properties) {
2472
+ if (!BabelTypes.isObjectProperty(entry)) continue;
2473
+ const routeName = BabelTypes.isIdentifier(entry.key) ? entry.key.name : BabelTypes.isStringLiteral(entry.key) ? entry.key.value : null;
2474
+ if (!routeName) continue;
2475
+ const componentExpr = unwrapStaticScreen(entry.value);
2476
+ const componentFile = componentExpr ? this.graph.componentFile(filePath, componentExpr) : null;
2477
+ if (componentFile) this.routeTargets.set(routeName, componentFile);
2478
+ out.push({
2479
+ name: routeName,
2480
+ navigatorName,
2481
+ navigatorType,
2482
+ ...componentFile ? { componentFile } : {}
2483
+ });
2484
+ }
2485
+ }
2486
+ return out;
2487
+ }
2488
+ /**
2489
+ * Todo arquivo-fonte do projeto — não os que ficam num diretório com o nome
2490
+ * certo.
2491
+ *
2492
+ * POR QUE ISTO MUDOU. A lista anterior era `**\/navigation/**`,
2493
+ * `**\/navigator*` e `**\/routes*`, o que fazia da descoberta de navegação uma
2494
+ * convenção de caminho. Medido contra 20 apps React Native de terceiros
2495
+ * (`qa/eval-corpus`), o filtro errava por motivos que nada têm a ver com o
2496
+ * app não ter navegação:
2497
+ *
2498
+ * - `pocketpal` declara 4 navegadores em `App.tsx` e dentro de `src/screens/`;
2499
+ * - `comapeo` usa `src/frontend/Navigation/` — `N` maiúsculo, e o glob é
2500
+ * sensível a caixa;
2501
+ * - `abacus` usa `src/routes/index.tsx` — `routes` é o DIRETÓRIO, e o glob
2502
+ * pedia um arquivo chamado `routes*`;
2503
+ * - `discourse` declara em `js/Discourse.js` — o glob só aceitava `.ts`/`.tsx`;
2504
+ * - `rainbow` tem 11 arquivos com navegador e o glob alcançava 1.
2505
+ *
2506
+ * Varrer tudo não custa uma varredura nova: `ReactNativePlatformAnalyzer` já
2507
+ * globa e parseia a árvore inteira para `ComponentAnalyzer` e `FormAnalyzer`.
2508
+ * O que segura o custo aqui é o portão por evidência em `analyze()`, que só
2509
+ * parseia arquivo cujo texto menciona uma fábrica de navegador.
2510
+ *
2511
+ * Os globs antigos continuam na lista: se um projeto restringir `include`, o
2512
+ * que era encontrado antes continua sendo.
2513
+ */
1667
2514
  async findNavigationFiles() {
1668
2515
  const patterns = [
2516
+ ...this.config.include ?? ["**/*.{ts,tsx,js,jsx}"],
2517
+ // Piso de compatibilidade — nunca encontrar MENOS que a versão anterior.
1669
2518
  "**/navigation/**/*.{ts,tsx}",
1670
2519
  "**/navigator*.{ts,tsx}",
1671
2520
  "**/routes*.{ts,tsx}",
@@ -1676,91 +2525,188 @@ var NavigationAnalyzer = class {
1676
2525
  "**/node_modules/**",
1677
2526
  "**/dist/**",
1678
2527
  "**/build/**",
2528
+ // Um navegador declarado dentro de um teste é fixture, não a navegação
2529
+ // do app. Isto passou a importar quando a varredura deixou de ser por
2530
+ // caminho: em `expensify`, os únicos arquivos que o glob antigo
2531
+ // alcançava eram três de `tests/`.
2532
+ "**/__tests__/**",
2533
+ "**/test/**",
2534
+ "**/tests/**",
2535
+ "**/*.test.{ts,tsx,js,jsx}",
2536
+ "**/*.tests.{ts,tsx,js,jsx}",
2537
+ "**/*.spec.{ts,tsx,js,jsx}",
2538
+ // Sufixo `Test` sem ponto: `expensify` chama os fixtures dele de
2539
+ // `LegalNameStepTest.tsx`, e cada um monta um `<Stack.Screen>` próprio.
2540
+ // Auditando as 144 rotas lidas dele contra `SCREENS.ts`, 2 vinham daqui —
2541
+ // destinos que existem no teste e não no app, para os quais o agente
2542
+ // tentaria navegar.
2543
+ "**/*Test.{ts,tsx,js,jsx}",
2544
+ "**/*Tests.{ts,tsx,js,jsx}",
1679
2545
  ...this.config.exclude || [],
1680
2546
  // §5: Add user-configured navigation excludes
1681
2547
  ...this.navigationExclude
1682
2548
  ];
1683
- const files = await glob(patterns, {
2549
+ const files = await globSorted(patterns, {
1684
2550
  cwd: this.config.rootDir,
1685
2551
  ignore: excludePatterns
1686
2552
  });
1687
- return files.map((file) => path8__default.join(this.config.rootDir, file));
2553
+ return files.map((file) => path2__default.join(this.config.rootDir, file));
1688
2554
  }
1689
- /** Parse navigator definitions from a file */
1690
- parseNavigators(content, filePath) {
1691
- const navigators = [];
1692
- try {
1693
- const ast = parser.parse(content, {
1694
- sourceType: "module",
1695
- plugins: [
1696
- "jsx",
1697
- "typescript",
1698
- ...this.config.parserPlugins || []
1699
- ]
1700
- });
1701
- const navigatorCalls = /* @__PURE__ */ new Map();
1702
- const screensByNavigator = /* @__PURE__ */ new Map();
1703
- traverse4(ast, {
1704
- // Detect createStackNavigator / createTabNavigator / createDrawerNavigator calls
1705
- CallExpression: (nodePath) => {
1706
- const { node } = nodePath;
1707
- const callee = node.callee;
1708
- let navigatorType = null;
1709
- if (BabelTypes.isIdentifier(callee) && callee.name === "createNativeStackNavigator") {
1710
- navigatorType = "stack";
1711
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createStackNavigator") {
1712
- navigatorType = "stack";
1713
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createBottomTabNavigator") {
1714
- navigatorType = "tab";
1715
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createTabNavigator") {
1716
- navigatorType = "tab";
1717
- } else if (BabelTypes.isIdentifier(callee) && callee.name === "createDrawerNavigator") {
1718
- navigatorType = "drawer";
1719
- }
1720
- if (navigatorType) {
1721
- const parent = nodePath.parent;
1722
- if (BabelTypes.isVariableDeclarator(parent) && BabelTypes.isIdentifier(parent.id)) {
1723
- const varName = parent.id.name;
1724
- navigatorCalls.set(varName, {
1725
- name: varName,
1726
- type: navigatorType,
1727
- screens: []
1728
- });
1729
- }
1730
- }
1731
- },
1732
- // Detect Stack.Navigator / Tab.Navigator JSX elements
1733
- JSXElement: (nodePath) => {
1734
- const { node } = nodePath;
1735
- const openingElement = node.openingElement;
1736
- if (BabelTypes.isJSXMemberExpression(openingElement.name) && BabelTypes.isJSXIdentifier(openingElement.name.object)) {
1737
- const objectName = openingElement.name.object.name;
1738
- const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
1739
- if (propertyName === "Navigator") {
1740
- const navigator = navigatorCalls.get(objectName);
1741
- if (navigator) {
1742
- const initialRouteAttr = openingElement.attributes.find(
1743
- (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
1744
- );
1745
- if (BabelTypes.isJSXAttribute(initialRouteAttr) && BabelTypes.isStringLiteral(initialRouteAttr.value)) {
1746
- navigator.initialRouteName = initialRouteAttr.value.value;
1747
- }
1748
- const screens = this.extractScreensFromNavigator(node, objectName, navigator.type);
1749
- screensByNavigator.set(objectName, screens);
1750
- }
1751
- }
1752
- }
2555
+ /**
2556
+ * Fase 2: o JSX `<X.Navigator>` deste arquivo, ligado à declaração de `X` —
2557
+ * que pode estar aqui ou em qualquer arquivo que este importe.
2558
+ */
2559
+ parseNavigatorUsages(filePath, declarations) {
2560
+ const ast = this.graph.parse(filePath);
2561
+ if (!ast) return [];
2562
+ const found = /* @__PURE__ */ new Map();
2563
+ traverse4(ast, {
2564
+ JSXElement: (nodePath) => {
2565
+ const { node } = nodePath;
2566
+ const openingElement = node.openingElement;
2567
+ if (!BabelTypes.isJSXMemberExpression(openingElement.name) || !BabelTypes.isJSXIdentifier(openingElement.name.object)) {
2568
+ return;
1753
2569
  }
1754
- });
1755
- navigatorCalls.forEach((navigator) => {
1756
- const screens = screensByNavigator.get(navigator.name) || [];
1757
- navigator.screens = screens;
1758
- navigators.push(navigator);
1759
- });
1760
- } catch (error) {
1761
- console.warn(`Failed to parse navigators in ${filePath}:`, error);
2570
+ const objectName = openingElement.name.object.name;
2571
+ const propertyName = BabelTypes.isJSXIdentifier(openingElement.name.property) ? openingElement.name.property.name : null;
2572
+ if (propertyName !== "Navigator") return;
2573
+ const decl = this.resolveNavigator(filePath, objectName, declarations);
2574
+ if (!decl) return;
2575
+ const navigator = found.get(objectName) ?? {
2576
+ name: objectName,
2577
+ type: decl.type,
2578
+ screens: []
2579
+ };
2580
+ const initialRouteAttr = openingElement.attributes.find(
2581
+ (attr) => BabelTypes.isJSXAttribute(attr) && BabelTypes.isJSXIdentifier(attr.name) && attr.name.name === "initialRouteName"
2582
+ );
2583
+ if (BabelTypes.isJSXAttribute(initialRouteAttr)) {
2584
+ const initial = this.attributeString(filePath, initialRouteAttr);
2585
+ if (initial) navigator.initialRouteName = initial;
2586
+ }
2587
+ this.navigatorFiles.add(filePath);
2588
+ this.navigatorFiles.add(decl.file);
2589
+ navigator.screens = this.extractScreensFromNavigator(filePath, node, objectName, decl.type);
2590
+ found.set(objectName, navigator);
2591
+ }
2592
+ });
2593
+ return [...found.values()];
2594
+ }
2595
+ /**
2596
+ * Rotas declaradas FORA de qualquer `<X.Navigator>`.
2597
+ *
2598
+ * O CASO. A fase 2 desce a partir do `<X.Navigator>` e lê as `<X.Screen>`
2599
+ * que estão DENTRO dele. Dois alvos do corpus não escrevem assim, e entre os
2600
+ * dois são 171 rotas invisíveis:
2601
+ *
2602
+ * `bluesky` — `function commonScreens(Stack: typeof Flat) { return (<>
2603
+ * <Stack.Screen name="NotFound" … /> … </>) }`, chamada de
2604
+ * dentro de seis navegadores diferentes. 70 rotas.
2605
+ * `comapeo` — `export const createAppScreens = ({intl}) => (<>
2606
+ * <RootStack.Group><RootStack.Screen … /></RootStack.Group></>)`,
2607
+ * num arquivo sem `<RootStack.Navigator>` nenhum. 101 rotas.
2608
+ *
2609
+ * A REGRA. Uma `<X.Screen name="…">` sem `<Y.Navigator>` ancestral é rota do
2610
+ * navegador ao qual `X` se resolve. Não há palpite em jogo: o nome da rota é
2611
+ * literal do fonte (ou constante que o resolvedor segue), e `X` precisa
2612
+ * chegar a uma declaração de navegador que já existe. O que não resolve não
2613
+ * vira nada.
2614
+ *
2615
+ * O ancestral é o que evita contar duas vezes — dentro do `<X.Navigator>` a
2616
+ * fase 2 já leu, e somar aqui duplicaria cada rota do corpus inteiro.
2617
+ *
2618
+ * DUAS FORMAS DE RESOLVER `X`, e a segunda é o que o `bluesky` exige. A
2619
+ * primeira é a de sempre (declaração local, ou `import` seguido até a
2620
+ * origem) e resolve o `comapeo`. A segunda lê a ANOTAÇÃO DE TIPO do
2621
+ * parâmetro: em `commonScreens(Stack: typeof Flat)`, quem diz que `Stack` é
2622
+ * o navegador `Flat` é o próprio app, no fonte.
2623
+ *
2624
+ * O QUE ISTO NÃO FAZ. As 70 do `bluesky` ficam atribuídas a `Flat` — que as
2625
+ * contém de fato (`{commonScreens(Flat, numUnread)}`) — e não aos outros
2626
+ * cinco stacks que também chamam a mesma função. Seguir os seis pontos de
2627
+ * chamada é análise interprocedural, e o ganho seria só de atribuição: o nome
2628
+ * da rota e o arquivo da tela, que é o que o agente usa, já saem certos.
2629
+ */
2630
+ parseDetachedScreens(filePath, declarations) {
2631
+ const ast = this.graph.parse(filePath);
2632
+ if (!ast) return [];
2633
+ const byNavigator = /* @__PURE__ */ new Map();
2634
+ const resolved = /* @__PURE__ */ new Map();
2635
+ traverse4(ast, {
2636
+ JSXElement: (nodePath) => {
2637
+ const name = nodePath.node.openingElement.name;
2638
+ if (!BabelTypes.isJSXMemberExpression(name) || !BabelTypes.isJSXIdentifier(name.object)) return;
2639
+ if (!BabelTypes.isJSXIdentifier(name.property) || name.property.name !== "Screen") return;
2640
+ const insideNavigator = nodePath.findParent((parent) => {
2641
+ if (!parent.isJSXElement()) return false;
2642
+ const parentName = parent.node.openingElement.name;
2643
+ return BabelTypes.isJSXMemberExpression(parentName) && BabelTypes.isJSXIdentifier(parentName.property) && parentName.property.name === "Navigator";
2644
+ });
2645
+ if (insideNavigator) return;
2646
+ const local = name.object.name;
2647
+ if (!resolved.has(local)) {
2648
+ resolved.set(
2649
+ local,
2650
+ this.resolveDetachedNavigator(filePath, local, nodePath, declarations)
2651
+ );
2652
+ }
2653
+ const decl = resolved.get(local);
2654
+ if (!decl) return;
2655
+ const screen = this.parseScreenElement(filePath, nodePath.node, decl.name, decl.type);
2656
+ if (!screen) return;
2657
+ const nav = byNavigator.get(decl.name) ?? {
2658
+ name: decl.name,
2659
+ type: decl.type,
2660
+ screens: []
2661
+ };
2662
+ if (!nav.screens.some((existing) => existing.name === screen.name))
2663
+ nav.screens.push(screen);
2664
+ byNavigator.set(decl.name, nav);
2665
+ }
2666
+ });
2667
+ return [...byNavigator.values()];
2668
+ }
2669
+ /**
2670
+ * De um `X` usado como `<X.Screen>` até a declaração do navegador.
2671
+ *
2672
+ * Além do caminho normal — declaração local ou `import` seguido até a origem
2673
+ * — aceita `X` como PARÂMETRO anotado com `typeof Y`. É o idioma do
2674
+ * `bluesky`, e a anotação é declaração do app: nada aqui é inferido do nome.
2675
+ */
2676
+ resolveDetachedNavigator(filePath, localName, nodePath, declarations) {
2677
+ const direct = this.resolveNavigator(filePath, localName, declarations);
2678
+ if (direct) return direct;
2679
+ const binding = nodePath.scope.getBinding(localName);
2680
+ if (!binding || binding.kind !== "param") return null;
2681
+ const param = binding.path.node;
2682
+ if (!BabelTypes.isIdentifier(param) || !BabelTypes.isTSTypeAnnotation(param.typeAnnotation)) return null;
2683
+ const annotation = param.typeAnnotation.typeAnnotation;
2684
+ if (!BabelTypes.isTSTypeQuery(annotation) || !BabelTypes.isIdentifier(annotation.exprName)) return null;
2685
+ return this.resolveNavigator(filePath, annotation.exprName.name, declarations);
2686
+ }
2687
+ /**
2688
+ * De `<X.Navigator>` até a declaração de `X`.
2689
+ *
2690
+ * Primeiro no próprio arquivo; se não estiver, segue o `import` até onde `X`
2691
+ * é definido. Resolver pelo import é o que torna a junção entre arquivos
2692
+ * segura: dois `Stack` de arquivos diferentes nunca colidem, porque a chave
2693
+ * é o arquivo de DEFINIÇÃO.
2694
+ */
2695
+ resolveNavigator(filePath, localName, declarations) {
2696
+ const local = declarations.get(`${filePath}#${localName}`);
2697
+ if (local) return local;
2698
+ const origin = this.graph.resolveBinding(filePath, localName);
2699
+ if (!origin) return null;
2700
+ return declarations.get(`${origin.file}#${origin.name}`) ?? null;
2701
+ }
2702
+ /** O valor string de um atributo JSX, resolvendo constante importada. */
2703
+ attributeString(filePath, attr) {
2704
+ const value = attr.value;
2705
+ if (BabelTypes.isStringLiteral(value)) return value.value;
2706
+ if (BabelTypes.isJSXExpressionContainer(value) && BabelTypes.isExpression(value.expression)) {
2707
+ return this.graph.stringConstant(filePath, value.expression);
1762
2708
  }
1763
- return navigators;
2709
+ return null;
1764
2710
  }
1765
2711
  /**
1766
2712
  * Is this JSX element `<navigatorVarName.MEMBER …>`?
@@ -1855,22 +2801,48 @@ var NavigationAnalyzer = class {
1855
2801
  return found;
1856
2802
  }
1857
2803
  /** Extract screens from a navigator JSX element */
1858
- extractScreensFromNavigator(navigatorElement, navigatorVarName, navigatorType) {
2804
+ extractScreensFromNavigator(filePath, navigatorElement, navigatorVarName, navigatorType) {
1859
2805
  const screens = [];
1860
2806
  if (!navigatorElement.children) return screens;
1861
2807
  const seen = /* @__PURE__ */ new Set();
1862
2808
  for (const element of this.collectScreenElements(navigatorElement.children, navigatorVarName)) {
1863
- const screenName = this.extractAttributeValue(element.openingElement.attributes, "name");
1864
- if (!screenName || seen.has(screenName)) continue;
1865
- seen.add(screenName);
1866
- screens.push({
1867
- name: screenName,
1868
- navigatorName: navigatorVarName,
1869
- navigatorType
1870
- });
2809
+ const screen = this.parseScreenElement(filePath, element, navigatorVarName, navigatorType);
2810
+ if (!screen || seen.has(screen.name)) continue;
2811
+ seen.add(screen.name);
2812
+ screens.push(screen);
1871
2813
  }
1872
2814
  return screens;
1873
2815
  }
2816
+ /**
2817
+ * Uma `<X.Screen>` isolada até a rota que ela declara.
2818
+ *
2819
+ * Separado de `extractScreensFromNavigator` porque a MESMA leitura serve para
2820
+ * a `<X.Screen>` que mora dentro do `<X.Navigator>` e para a que mora fora
2821
+ * dele — só a forma de chegar até o elemento muda.
2822
+ */
2823
+ parseScreenElement(filePath, element, navigatorName, navigatorType) {
2824
+ const attrs = element.openingElement.attributes;
2825
+ const nameAttr = attrs.find(
2826
+ (a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "name"
2827
+ );
2828
+ const screenName = BabelTypes.isJSXAttribute(nameAttr) ? this.attributeString(filePath, nameAttr) : null;
2829
+ if (!screenName) return null;
2830
+ const componentAttr = attrs.find(
2831
+ (a) => BabelTypes.isJSXAttribute(a) && BabelTypes.isJSXIdentifier(a.name) && a.name.name === "component"
2832
+ );
2833
+ let componentFile = null;
2834
+ if (BabelTypes.isJSXAttribute(componentAttr) && BabelTypes.isJSXExpressionContainer(componentAttr.value)) {
2835
+ const expr = componentAttr.value.expression;
2836
+ if (BabelTypes.isExpression(expr)) componentFile = this.graph.componentFile(filePath, expr);
2837
+ }
2838
+ if (componentFile) this.routeTargets.set(screenName, componentFile);
2839
+ return {
2840
+ name: screenName,
2841
+ navigatorName,
2842
+ navigatorType,
2843
+ ...componentFile ? { componentFile } : {}
2844
+ };
2845
+ }
1874
2846
  /** Extract string attribute value from JSX attributes */
1875
2847
  extractAttributeValue(attributes, attrName) {
1876
2848
  const attr = attributes.find(
@@ -1966,7 +2938,7 @@ var NavigationAnalyzer = class {
1966
2938
  if (type.type === "TSUndefinedKeyword") return "undefined";
1967
2939
  if (type.type === "TSNullKeyword") return "null";
1968
2940
  if (type.type === "TSUnionType") {
1969
- return type.types.map((t12) => this.typeToString(t12)).join(" | ");
2941
+ return type.types.map((t13) => this.typeToString(t13)).join(" | ");
1970
2942
  }
1971
2943
  if (type.type === "TSTypeLiteral") {
1972
2944
  return "object";
@@ -1987,7 +2959,7 @@ var NavigationAnalyzer = class {
1987
2959
  /** Attach parsed type params to navigator screens */
1988
2960
  attachParamsToNavigators(navigators, types) {
1989
2961
  for (const navigator of navigators) {
1990
- const matchingType = types.find((t12) => t12.type === navigator.type);
2962
+ const matchingType = types.find((t13) => t13.type === navigator.type);
1991
2963
  if (matchingType) {
1992
2964
  for (const screen of navigator.screens) {
1993
2965
  const screenParams = matchingType.paramEntries.get(screen.name);
@@ -2077,8 +3049,8 @@ var ComponentAnalyzer = class {
2077
3049
  });
2078
3050
  const components = [];
2079
3051
  traverse4(ast, {
2080
- JSXElement: (path9) => {
2081
- const component = this.extractComponentFromJSXElement(path9.node);
3052
+ JSXElement: (path11) => {
3053
+ const component = this.extractComponentFromJSXElement(path11.node);
2082
3054
  if (component) {
2083
3055
  components.push(component);
2084
3056
  }
@@ -2205,13 +3177,13 @@ var FormAnalyzer = class {
2205
3177
  this.inputElements = [];
2206
3178
  this.submitButtons = [];
2207
3179
  traverse4(ast, {
2208
- CallExpression: (path9) => {
2209
- this.extractStateVariables(path9.node);
3180
+ CallExpression: (path11) => {
3181
+ this.extractStateVariables(path11.node);
2210
3182
  }
2211
3183
  });
2212
3184
  traverse4(ast, {
2213
- JSXElement: (path9) => {
2214
- this.extractFormElements(path9.node);
3185
+ JSXElement: (path11) => {
3186
+ this.extractFormElements(path11.node);
2215
3187
  }
2216
3188
  });
2217
3189
  const validationRules = this.extractValidationRules(ast);
@@ -2311,8 +3283,8 @@ var FormAnalyzer = class {
2311
3283
  extractValidationRules(ast) {
2312
3284
  const rules = {};
2313
3285
  traverse4(ast, {
2314
- IfStatement: (path9) => {
2315
- const test = path9.node.test;
3286
+ IfStatement: (path11) => {
3287
+ const test = path11.node.test;
2316
3288
  const rule = this.extractRuleFromCondition(test);
2317
3289
  if (rule) {
2318
3290
  const { field, description } = rule;
@@ -2366,7 +3338,7 @@ var FormAnalyzer = class {
2366
3338
  }
2367
3339
  buildForms(filePath, validationRules) {
2368
3340
  if (this.inputElements.length === 0) return [];
2369
- const fileName = path8.basename(filePath, path8.extname(filePath));
3341
+ const fileName = path2.basename(filePath, path2.extname(filePath));
2370
3342
  const formId = `${fileName}Form`.replace(/Screen$/, "").toLowerCase();
2371
3343
  const fields = this.inputElements.map((input) => {
2372
3344
  const fieldType = this.inferFieldType(input);
@@ -2415,13 +3387,105 @@ var FormAnalyzer = class {
2415
3387
  return rule !== void 0 && rule.includes("required");
2416
3388
  }
2417
3389
  };
3390
+
3391
+ // src/pipeline/composition.ts
3392
+ var MAX_DEPTH = 3;
3393
+ var MAX_VISITED_PER_SCREEN = 60;
3394
+ var MAX_FAN_IN = 3;
3395
+ var ADDRESSABLE = /* @__PURE__ */ new Set(["testID", "appilotsId", "accessibilityLabel", "label"]);
3396
+ function descendants(graph, from, screenFiles) {
3397
+ const seen = /* @__PURE__ */ new Set([from]);
3398
+ const out = [];
3399
+ let frontier = [from];
3400
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
3401
+ const next = [];
3402
+ for (const file of frontier) {
3403
+ for (const child of graph.renderedComponentFiles(file)) {
3404
+ if (seen.has(child)) continue;
3405
+ seen.add(child);
3406
+ if (screenFiles.has(child)) continue;
3407
+ out.push(child);
3408
+ next.push(child);
3409
+ if (out.length >= MAX_VISITED_PER_SCREEN) return out;
3410
+ }
3411
+ }
3412
+ frontier = next;
3413
+ }
3414
+ return out;
3415
+ }
3416
+ function addressable(locator) {
3417
+ if (!locator || typeof locator !== "object") return false;
3418
+ const l = locator;
3419
+ if (typeof l["source"] === "string" && ADDRESSABLE.has(l["source"])) return true;
3420
+ return typeof l["testID"] === "string" && l["testID"].length > 0;
3421
+ }
3422
+ async function composeAffordances(options) {
3423
+ const { screens, graph, analyzeFile, screenFiles } = options;
3424
+ const byScreen = /* @__PURE__ */ new Map();
3425
+ const fanIn = /* @__PURE__ */ new Map();
3426
+ for (const screen of screens) {
3427
+ if (!screen.filePath) continue;
3428
+ const children = descendants(graph, screen.filePath, screenFiles);
3429
+ byScreen.set(screen, children);
3430
+ for (const child of children) fanIn.set(child, (fanIn.get(child) ?? 0) + 1);
3431
+ }
3432
+ const analyzed = /* @__PURE__ */ new Map();
3433
+ let screensEnriched = 0;
3434
+ let filesMerged = 0;
3435
+ for (const [screen, children] of byScreen) {
3436
+ const exclusive = children.filter((c) => (fanIn.get(c) ?? 0) <= MAX_FAN_IN);
3437
+ if (exclusive.length === 0) continue;
3438
+ const actionIds = new Set(screen.actions.map((a) => a.id));
3439
+ const targetIds = new Set((screen.targets ?? []).map((t13) => t13.id));
3440
+ const formIds = new Set(screen.forms.map((f) => f.id));
3441
+ let gained = false;
3442
+ for (const file of exclusive) {
3443
+ if (!analyzed.has(file)) {
3444
+ try {
3445
+ analyzed.set(file, await analyzeFile(file));
3446
+ } catch {
3447
+ analyzed.set(file, null);
3448
+ }
3449
+ }
3450
+ const child = analyzed.get(file);
3451
+ if (!child) continue;
3452
+ let merged = false;
3453
+ for (const action of child.actions) {
3454
+ if (!addressable(action.locator)) continue;
3455
+ if (actionIds.has(action.id)) continue;
3456
+ actionIds.add(action.id);
3457
+ screen.actions.push(action);
3458
+ merged = true;
3459
+ }
3460
+ for (const target of child.targets ?? []) {
3461
+ if (!addressable(target.locator)) continue;
3462
+ if (targetIds.has(target.id)) continue;
3463
+ targetIds.add(target.id);
3464
+ (screen.targets ??= []).push(target);
3465
+ merged = true;
3466
+ }
3467
+ for (const form of child.forms) {
3468
+ if (form.fields.length === 0) continue;
3469
+ const id = formIds.has(form.id) ? `${form.id}:${child.name}` : form.id;
3470
+ if (formIds.has(id)) continue;
3471
+ formIds.add(id);
3472
+ screen.forms.push({ ...form, id });
3473
+ merged = true;
3474
+ }
3475
+ if (merged) {
3476
+ filesMerged++;
3477
+ gained = true;
3478
+ }
3479
+ }
3480
+ if (gained) screensEnriched++;
3481
+ }
3482
+ return { screens, screensEnriched, filesMerged };
3483
+ }
3484
+
3485
+ // src/analyzers/ReactNativePlatformAnalyzer.ts
2418
3486
  var ReactNativePlatformAnalyzer = class {
2419
3487
  platform = "react-native";
2420
3488
  async analyze(config, options) {
2421
- const screenAnalyzer = new ScreenAnalyzer(config, {
2422
- strictScreens: options.strictScreens ?? true,
2423
- screenPatterns: options.screenPatterns
2424
- });
2425
3489
  const navigationAnalyzer = new NavigationAnalyzer(config, {
2426
3490
  navigationInclude: options.navigationInclude,
2427
3491
  navigationExclude: options.navigationExclude
@@ -2429,14 +3493,31 @@ var ReactNativePlatformAnalyzer = class {
2429
3493
  const componentAnalyzer = new ComponentAnalyzer(config);
2430
3494
  const formAnalyzer = new FormAnalyzer(config);
2431
3495
  console.log("[ReactNativePlatformAnalyzer] Running analyzers...");
2432
- const [screens, navigation] = await Promise.all([
2433
- screenAnalyzer.analyze(),
2434
- navigationAnalyzer.analyze()
2435
- ]);
3496
+ const navigation = await navigationAnalyzer.analyze();
3497
+ const screenAnalyzer = new ScreenAnalyzer(config, {
3498
+ strictScreens: options.strictScreens ?? true,
3499
+ screenPatterns: options.screenPatterns,
3500
+ // Menos os arquivos que declaram navegador: ver `navigatorFiles`.
3501
+ routeTargetFiles: new Set(
3502
+ [...navigationAnalyzer.routeTargets.values()].filter(
3503
+ (file) => !navigationAnalyzer.navigatorFiles.has(file)
3504
+ )
3505
+ )
3506
+ });
3507
+ const screens = await screenAnalyzer.analyze();
3508
+ const composed = await composeAffordances({
3509
+ screens,
3510
+ graph: navigationAnalyzer.graph,
3511
+ analyzeFile: (file) => screenAnalyzer.analyzeFile(file),
3512
+ screenFiles: new Set(screens.map((s) => s.filePath).filter(Boolean))
3513
+ });
3514
+ console.log(
3515
+ `[ReactNativePlatformAnalyzer] Composi\xE7\xE3o: ${composed.filesMerged} componente(s) exclusivo(s) fundido(s) em ${composed.screensEnriched} tela(s)`
3516
+ );
2436
3517
  console.log(
2437
3518
  `[ReactNativePlatformAnalyzer] Screen and navigation analysis complete. Found ${screens.length} screens`
2438
3519
  );
2439
- const screenFiles = await glob(config.include || ["**/*.tsx", "**/*.ts"], {
3520
+ const screenFiles = await globSorted(config.include || ["**/*.tsx", "**/*.ts"], {
2440
3521
  cwd: config.rootDir,
2441
3522
  ignore: config.exclude || ["**/node_modules/**"]
2442
3523
  });
@@ -2444,7 +3525,7 @@ var ReactNativePlatformAnalyzer = class {
2444
3525
  `[ReactNativePlatformAnalyzer] Analyzing components and forms from ${screenFiles.length} files...`
2445
3526
  );
2446
3527
  const enrichmentPromises = screenFiles.map(async (file) => {
2447
- const filePath = path8__default.resolve(config.rootDir, file);
3528
+ const filePath = path2__default.resolve(config.rootDir, file);
2448
3529
  try {
2449
3530
  const [components, forms] = await Promise.all([
2450
3531
  componentAnalyzer.analyzeFile(filePath),
@@ -2499,7 +3580,8 @@ var ReactNativePlatformAnalyzer = class {
2499
3580
  screens: enrichedScreens,
2500
3581
  navigation,
2501
3582
  analyzedFiles: screenFiles.length,
2502
- ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {}
3583
+ ...screenAnalyzer.screensFilteredOut > 0 ? { screensFilteredOut: screenAnalyzer.screensFilteredOut } : {},
3584
+ diagnostics: { navigation: navigationAnalyzer.diagnostics }
2503
3585
  };
2504
3586
  }
2505
3587
  mergeForm(target, source) {
@@ -2517,9 +3599,12 @@ var ReactNativePlatformAnalyzer = class {
2517
3599
  findEquivalentField(fields, incoming) {
2518
3600
  return fields.find((field) => {
2519
3601
  if (field.name && incoming.name && field.name === incoming.name) return true;
2520
- if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding) return true;
2521
- if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id) return true;
2522
- if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder) return true;
3602
+ if (field.valueBinding && incoming.valueBinding && field.valueBinding === incoming.valueBinding)
3603
+ return true;
3604
+ if (field.locator?.id && incoming.locator?.id && field.locator.id === incoming.locator.id)
3605
+ return true;
3606
+ if (field.placeholder && incoming.placeholder && field.placeholder === incoming.placeholder)
3607
+ return true;
2523
3608
  if (field.locator?.accessibilityLabel && incoming.locator?.accessibilityLabel && field.locator.accessibilityLabel === incoming.locator.accessibilityLabel) {
2524
3609
  return true;
2525
3610
  }
@@ -2552,7 +3637,9 @@ var ReactNativePlatformAnalyzer = class {
2552
3637
  if (incoming.fields.length === 0) return void 0;
2553
3638
  let best;
2554
3639
  for (const form of forms) {
2555
- const overlap = incoming.fields.filter((field) => this.findEquivalentField(form.fields, field)).length;
3640
+ const overlap = incoming.fields.filter(
3641
+ (field) => this.findEquivalentField(form.fields, field)
3642
+ ).length;
2556
3643
  if (overlap > 0 && (!best || overlap > best.overlap)) {
2557
3644
  best = { form, overlap };
2558
3645
  }
@@ -2706,13 +3793,13 @@ function staticRoutePath(node) {
2706
3793
  if (!node) return void 0;
2707
3794
  if (BabelTypes.isStringLiteral(node)) return node.value;
2708
3795
  if (BabelTypes.isTemplateLiteral(node)) {
2709
- let path9 = "";
3796
+ let path11 = "";
2710
3797
  node.quasis.forEach((quasi, index) => {
2711
- path9 += quasi.value.cooked ?? quasi.value.raw;
3798
+ path11 += quasi.value.cooked ?? quasi.value.raw;
2712
3799
  const expr = node.expressions[index];
2713
- if (expr) path9 += `:${paramNameOf(expr)}`;
3800
+ if (expr) path11 += `:${paramNameOf(expr)}`;
2714
3801
  });
2715
- return path9;
3802
+ return path11;
2716
3803
  }
2717
3804
  return void 0;
2718
3805
  }
@@ -2797,17 +3884,17 @@ var WebScreenAnalyzer = class {
2797
3884
  include = ["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
2798
3885
  exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]
2799
3886
  } = this.config;
2800
- const files = (await glob(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
3887
+ const files = (await globSorted(include, { cwd: this.config.rootDir, ignore: exclude })).filter(
2801
3888
  (file) => !file.endsWith(".d.ts")
2802
3889
  );
2803
- const patternMatches = await glob(this.screenPatterns, {
3890
+ const patternMatches = await globSorted(this.screenPatterns, {
2804
3891
  cwd: this.config.rootDir,
2805
3892
  ignore: exclude
2806
3893
  });
2807
- const patternSet = new Set(patternMatches.map((f) => path8__default.resolve(this.config.rootDir, f)));
3894
+ const patternSet = new Set(patternMatches.map((f) => path2__default.resolve(this.config.rootDir, f)));
2808
3895
  const candidates = [];
2809
3896
  for (const file of files) {
2810
- const filePath = path8__default.resolve(this.config.rootDir, file);
3897
+ const filePath = path2__default.resolve(this.config.rootDir, file);
2811
3898
  try {
2812
3899
  const candidate = await this.analyzeFile(filePath);
2813
3900
  if (!candidate) continue;
@@ -2851,7 +3938,7 @@ var WebScreenAnalyzer = class {
2851
3938
  }
2852
3939
  }
2853
3940
  }
2854
- const name = registerScreenMeta?.name || componentName || path8__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
3941
+ const name = registerScreenMeta?.name || componentName || path2__default.basename(filePath).replace(/\.(tsx?|jsx?)$/, "");
2855
3942
  const descriptor = {
2856
3943
  name,
2857
3944
  filePath,
@@ -3740,7 +4827,9 @@ var WebNavigationAnalyzer = class {
3740
4827
  for (const filePath of files) {
3741
4828
  try {
3742
4829
  const content = await promises.readFile(filePath, "utf-8");
3743
- if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(content)) {
4830
+ if (!/createBrowserRouter|createHashRouter|createMemoryRouter|useRoutes|<Route[\s>]/.test(
4831
+ content
4832
+ )) {
3744
4833
  continue;
3745
4834
  }
3746
4835
  const ast = parseSource(content, this.config.parserPlugins);
@@ -3770,8 +4859,8 @@ var WebNavigationAnalyzer = class {
3770
4859
  ...this.config.exclude || [],
3771
4860
  ...this.navigationExclude
3772
4861
  ];
3773
- const files = await glob(patterns, { cwd: this.config.rootDir, ignore });
3774
- return files.map((file) => path8__default.join(this.config.rootDir, file));
4862
+ const files = await globSorted(patterns, { cwd: this.config.rootDir, ignore });
4863
+ return files.map((file) => path2__default.join(this.config.rootDir, file));
3775
4864
  }
3776
4865
  // ── JSX <Route> style ────────────────────────────────────────────
3777
4866
  extractJsxRoutes(ast) {
@@ -3943,11 +5032,13 @@ var WebNavigationAnalyzer = class {
3943
5032
  return {
3944
5033
  screens,
3945
5034
  initialScreen: initialRoute?.screenName ?? "",
3946
- navigators: routes.length > 0 ? [{
3947
- name: navigatorName,
3948
- type: WEB_NAVIGATOR_TYPE,
3949
- screens: screenNames
3950
- }] : []
5035
+ navigators: routes.length > 0 ? [
5036
+ {
5037
+ name: navigatorName,
5038
+ type: WEB_NAVIGATOR_TYPE,
5039
+ screens: screenNames
5040
+ }
5041
+ ] : []
3951
5042
  };
3952
5043
  }
3953
5044
  };
@@ -4269,8 +5360,8 @@ var ZodParsedType = util.arrayToEnum([
4269
5360
  "set"
4270
5361
  ]);
4271
5362
  var getParsedType = (data) => {
4272
- const t12 = typeof data;
4273
- switch (t12) {
5363
+ const t13 = typeof data;
5364
+ switch (t13) {
4274
5365
  case "undefined":
4275
5366
  return ZodParsedType.undefined;
4276
5367
  case "string":
@@ -4542,8 +5633,8 @@ function getErrorMap() {
4542
5633
 
4543
5634
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4544
5635
  var makeIssue = (params) => {
4545
- const { data, path: path9, errorMaps, issueData } = params;
4546
- const fullPath = [...path9, ...issueData.path || []];
5636
+ const { data, path: path11, errorMaps, issueData } = params;
5637
+ const fullPath = [...path11, ...issueData.path || []];
4547
5638
  const fullIssue = {
4548
5639
  ...issueData,
4549
5640
  path: fullPath
@@ -4659,11 +5750,11 @@ var errorUtil;
4659
5750
 
4660
5751
  // ../../node_modules/zod/v3/types.js
4661
5752
  var ParseInputLazyPath = class {
4662
- constructor(parent, value, path9, key) {
5753
+ constructor(parent, value, path11, key) {
4663
5754
  this._cachedPath = [];
4664
5755
  this.parent = parent;
4665
5756
  this.data = value;
4666
- this._path = path9;
5757
+ this._path = path11;
4667
5758
  this._key = key;
4668
5759
  }
4669
5760
  get path() {
@@ -8104,7 +9195,18 @@ var coerce = {
8104
9195
  };
8105
9196
  var NEVER = INVALID;
8106
9197
 
8107
- // ../shared/dist/chunk-MKT45LRA.mjs
9198
+ // ../shared/dist/chunk-SWQOZWHP.mjs
9199
+ var SAFE_IMAGE_URL_RE = /^(?:https?:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i;
9200
+ var HAS_SCHEME_RE = /^[\s\u0000-\u001f]*[a-z][a-z0-9+.-]*:/i;
9201
+ function isSafeImageUrl(value) {
9202
+ if (value === null || value === void 0) return true;
9203
+ const trimmed = value.trim();
9204
+ if (trimmed === "") return true;
9205
+ if (!HAS_SCHEME_RE.test(trimmed)) return true;
9206
+ return SAFE_IMAGE_URL_RE.test(trimmed);
9207
+ }
9208
+
9209
+ // ../shared/dist/chunk-NMQNNPSJ.mjs
8108
9210
  var locatorSourceSchema = external_exports.enum([
8109
9211
  "appilotsId",
8110
9212
  "testID",
@@ -8283,6 +9385,23 @@ var actionDescriptorSchema = external_exports.object({
8283
9385
  effect: external_exports.enum(["read", "write", "destructive"]).or(external_exports.string()).optional(),
8284
9386
  riskLevel: external_exports.enum(["low", "medium", "high"]).or(external_exports.string()).optional(),
8285
9387
  requiresConfirmation: external_exports.boolean().optional(),
9388
+ /**
9389
+ * How long this action's work is expected to take, in milliseconds,
9390
+ * DECLARED by the app — not inferred (that is `appilotsInferred`).
9391
+ *
9392
+ * The SDK's post-action wait was a constant: 6s, or 10s when the
9393
+ * generator could prove the handler awaits something. No constant
9394
+ * fits, because app operations run from ~100ms to minutes, and the
9395
+ * failure is silent in both directions — too short and the agent
9396
+ * photographs a loading screen with no controls on it, too long and
9397
+ * every fast press pays for the slowest one. Only the app knows.
9398
+ *
9399
+ * Absent means absent: the SDK keeps its current defaults, so a
9400
+ * document generated before this field existed behaves exactly as it
9401
+ * did. The SDK also clamps the value to its own safety ceiling — a
9402
+ * declared budget is a request, not a licence to hang the session.
9403
+ */
9404
+ asyncBudgetMs: external_exports.number().int().positive().optional(),
8286
9405
  appilotsInferred: appilotsInferredActionSchema.optional()
8287
9406
  }).passthrough();
8288
9407
  var screenPermissionDescriptorSchema = external_exports.object({
@@ -8465,7 +9584,7 @@ external_exports.object({
8465
9584
  version: external_exports.string().default("1.0"),
8466
9585
  content: external_exports.record(external_exports.unknown())
8467
9586
  });
8468
- var apiKeyScopeSchema = external_exports.enum(["sdk", "operator"]);
9587
+ var apiKeyScopeSchema = external_exports.enum(["sdk", "operator", "publish"]);
8469
9588
  var apiKeyEnvironmentSchema = external_exports.enum(["test", "live"]);
8470
9589
  external_exports.object({
8471
9590
  name: external_exports.string().min(1).max(100),
@@ -8476,8 +9595,8 @@ external_exports.object({
8476
9595
  scope: apiKeyScopeSchema.default("sdk"),
8477
9596
  environment: apiKeyEnvironmentSchema.optional(),
8478
9597
  expiresAt: external_exports.string().datetime().optional()
8479
- }).refine((v) => v.scope !== "sdk" || !!v.projectId, {
8480
- message: 'projectId is required for scope "sdk"',
9598
+ }).refine((v) => v.scope === "operator" || !!v.projectId, {
9599
+ message: "projectId is required for SDK and publishing keys",
8481
9600
  path: ["projectId"]
8482
9601
  });
8483
9602
  var boundedString = (max) => external_exports.string().max(max);
@@ -8502,6 +9621,15 @@ var snapshotInputSchema = external_exports.object({
8502
9621
  type: boundedString(40).optional(),
8503
9622
  required: external_exports.boolean().optional(),
8504
9623
  invalid: external_exports.boolean().optional(),
9624
+ /**
9625
+ * Whether the field holds anything, as its own fact rather than an
9626
+ * inference over `value`. The value is the user's and is the first
9627
+ * thing a privacy policy withholds; the existence of a value is the
9628
+ * agent's and is what stops it filling the same field twice.
9629
+ */
9630
+ filled: external_exports.boolean().optional(),
9631
+ /** This input has keyboard focus right now. */
9632
+ focused: external_exports.boolean().optional(),
8505
9633
  inModal: external_exports.boolean().optional()
8506
9634
  }).passthrough();
8507
9635
  var snapshotButtonSchema = external_exports.object({
@@ -8657,6 +9785,32 @@ var agentSnapshotSchema = external_exports.object({
8657
9785
  * it. Optional: older SDKs never clamp and never send it.
8658
9786
  */
8659
9787
  truncated: external_exports.boolean().optional(),
9788
+ /**
9789
+ * What moved since the previous observation, computed on the device
9790
+ * because that is the only side holding both snapshots.
9791
+ *
9792
+ * Shape only — counts, booleans, and ids the app declared itself — so
9793
+ * it survives an observation whose content was withheld. ABSENT means
9794
+ * there was no previous observation to compare against; `unchanged:
9795
+ * true` means we compared and nothing moved, which is the strongest
9796
+ * evidence available that an action did nothing. The two must not be
9797
+ * collapsed, for the same reason `truncated` exists.
9798
+ */
9799
+ delta: external_exports.object({
9800
+ routeChanged: external_exports.boolean().optional(),
9801
+ modalOpened: external_exports.boolean().optional(),
9802
+ modalClosed: external_exports.boolean().optional(),
9803
+ loadingStarted: external_exports.boolean().optional(),
9804
+ loadingFinished: external_exports.boolean().optional(),
9805
+ textsAdded: external_exports.number().int().nonnegative().optional(),
9806
+ textsRemoved: external_exports.number().int().nonnegative().optional(),
9807
+ visibleRowsDelta: external_exports.number().int().optional(),
9808
+ totalRowsDelta: external_exports.number().int().optional(),
9809
+ fieldsNewlyFilled: external_exports.array(boundedString(160)).max(12).optional(),
9810
+ fieldsCleared: external_exports.array(boundedString(160)).max(12).optional(),
9811
+ invalidAppeared: external_exports.boolean().optional(),
9812
+ unchanged: external_exports.boolean().optional()
9813
+ }).passthrough().optional(),
8660
9814
  /**
8661
9815
  * How many of this screen's controls the client could name, split by
8662
9816
  * `identityProvenance`. Diagnostic only — the relay never grounds an
@@ -8690,7 +9844,19 @@ var agentContextSchema = external_exports.object({
8690
9844
  rootRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8691
9845
  currentRouteNames: external_exports.array(boundedString(200)).max(200).optional(),
8692
9846
  routeNames: external_exports.array(boundedString(200)).max(500).optional(),
8693
- canGoBack: external_exports.boolean().optional()
9847
+ canGoBack: external_exports.boolean().optional(),
9848
+ /**
9849
+ * The stack a back press pops through, oldest first, ending on
9850
+ * the current screen. `canGoBack` says a back exists; this says
9851
+ * where it goes.
9852
+ */
9853
+ backStack: external_exports.array(boundedString(200)).max(50).optional(),
9854
+ /**
9855
+ * Screens the session has been on, oldest first. Names only —
9856
+ * params carry record ids and often personal data, and a route
9857
+ * name is structure.
9858
+ */
9859
+ visited: external_exports.array(boundedString(200)).max(8).optional()
8694
9860
  }).passthrough().optional(),
8695
9861
  screenMetadata: external_exports.object({
8696
9862
  name: boundedString(200).optional(),
@@ -8796,7 +9962,7 @@ var formFillPayloadSchema = external_exports.object({
8796
9962
  submitAfterFill: external_exports.boolean().optional()
8797
9963
  }).passthrough();
8798
9964
  var uiInteractionPayloadSchema = external_exports.object({
8799
- action: external_exports.enum(["press", "longPress", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
9965
+ action: external_exports.enum(["press", "longPress", "toggle", "scroll", "swipe", "focus", "set_value"]).default("press").optional(),
8800
9966
  // `targetId` is the canonical server/LLM field. SDK runtimes still
8801
9967
  // accept `componentId` as a compatibility alias.
8802
9968
  targetId: external_exports.string().min(1),
@@ -8964,8 +10130,15 @@ external_exports.object({
8964
10130
  // issue #169 — the observation the server grounds targets against, now
8965
10131
  // validated + bounded at the border instead of z.record(z.unknown()).
8966
10132
  context: agentContextSchema.optional(),
8967
- /** Hop counter — server enforces a cap to prevent runaway loops */
8968
- hop: external_exports.number().int().min(1).max(10).optional()
10133
+ /**
10134
+ * Hop counter — server enforces a cap to prevent runaway loops.
10135
+ * The max here must stay ABOVE the server's `MAX_AGENT_HOPS` (10 since
10136
+ * #503, apps/api/src/modules/agents/routes.ts): the first over-budget
10137
+ * hop has to get through validation so the route can answer it with
10138
+ * the friendly automation-limit message instead of a 422. The +3
10139
+ * headroom mirrors what 7/10 was before the recalibration.
10140
+ */
10141
+ hop: external_exports.number().int().min(1).max(13).optional()
8969
10142
  });
8970
10143
  var agentAccessLevelSchema = external_exports.enum(["read", "write", "none"]);
8971
10144
  var screenPermissionSchema = external_exports.object({
@@ -9071,15 +10244,18 @@ var themeTokensSchema = external_exports.object({
9071
10244
  */
9072
10245
  mode: external_exports.enum(["auto", "light", "dark"]).optional()
9073
10246
  }).strict();
10247
+ var imageSourceField = external_exports.string().max(500).refine(isSafeImageUrl, {
10248
+ message: "Only http(s) URLs, base64 image data URIs, emoji or asset ids are allowed here"
10249
+ });
9074
10250
  external_exports.object({
9075
10251
  /** Tone / persona instructions appended to the system prompt. */
9076
10252
  personaPrompt: external_exports.string().max(2048).nullable(),
9077
10253
  /** Display name in the chat header (e.g. "Aria"). */
9078
10254
  assistantName: external_exports.string().max(60).nullable(),
9079
10255
  /** URL or remote asset id for the avatar shown next to assistant turns. */
9080
- assistantAvatar: external_exports.string().max(500).nullable(),
10256
+ assistantAvatar: imageSourceField.nullable(),
9081
10257
  /** Emoji or image URL for the empty-state icon. Auto-detected by prefix. */
9082
- emptyStateIcon: external_exports.string().max(500).nullable(),
10258
+ emptyStateIcon: imageSourceField.nullable(),
9083
10259
  /** First-message text shown in the empty state. */
9084
10260
  welcomeMessage: external_exports.string().max(500).nullable(),
9085
10261
  /** Title rendered at the top of the chat. */
@@ -9093,20 +10269,20 @@ external_exports.object({
9093
10269
  /** Chat-open FAB background color. Null falls back to theme.colors.primary. */
9094
10270
  triggerButtonColor: hexColorSchema.nullable(),
9095
10271
  /** Image URL rendered inside the FAB instead of the default chat-bubble icon. */
9096
- triggerButtonImageUrl: external_exports.string().max(500).nullable()
10272
+ triggerButtonImageUrl: imageSourceField.nullable()
9097
10273
  });
9098
10274
  external_exports.object({
9099
10275
  personaPrompt: external_exports.string().max(2048).nullable().optional(),
9100
10276
  assistantName: external_exports.string().max(60).nullable().optional(),
9101
- assistantAvatar: external_exports.string().max(500).nullable().optional(),
9102
- emptyStateIcon: external_exports.string().max(500).nullable().optional(),
10277
+ assistantAvatar: imageSourceField.nullable().optional(),
10278
+ emptyStateIcon: imageSourceField.nullable().optional(),
9103
10279
  welcomeMessage: external_exports.string().max(500).nullable().optional(),
9104
10280
  chatTitle: external_exports.string().max(60).nullable().optional(),
9105
10281
  poweredByVisible: external_exports.boolean().optional(),
9106
10282
  theme: themeTokensSchema.nullable().optional(),
9107
10283
  defaultLocale: localeSchema.nullable().optional(),
9108
10284
  triggerButtonColor: hexColorSchema.nullable().optional(),
9109
- triggerButtonImageUrl: external_exports.string().max(500).nullable().optional()
10285
+ triggerButtonImageUrl: imageSourceField.nullable().optional()
9110
10286
  }).strict();
9111
10287
  var sandboxObservationSchema = external_exports.object({
9112
10288
  route: external_exports.string().max(200).optional(),
@@ -9126,7 +10302,19 @@ var sandboxObservationSchema = external_exports.object({
9126
10302
  type: external_exports.string().max(40).optional(),
9127
10303
  placeholder: external_exports.string().max(200).optional(),
9128
10304
  /** Inline validation state (SDK snapshot field). */
9129
- invalid: external_exports.boolean().optional()
10305
+ invalid: external_exports.boolean().optional(),
10306
+ /**
10307
+ * Whether the field holds anything, as its own fact rather than
10308
+ * an inference over `value`.
10309
+ *
10310
+ * Here because the eval posts through this schema: a fact the
10311
+ * SDK produces and this contract has no word for is a fact the
10312
+ * corpus can never grade the agent on. `invalid` and `required`
10313
+ * were already here; these two were the half that was missing.
10314
+ */
10315
+ filled: external_exports.boolean().optional(),
10316
+ /** This input has keyboard focus right now. */
10317
+ focused: external_exports.boolean().optional()
9130
10318
  })
9131
10319
  ).max(100).optional(),
9132
10320
  buttons: external_exports.array(
@@ -9605,7 +10793,7 @@ var manifestSchema = external_exports.object({
9605
10793
  navigation: navigationGraphSchema.partial().optional()
9606
10794
  }).passthrough();
9607
10795
  async function loadManifest(rootDir, manifestPath) {
9608
- const resolvedPath = path8__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
10796
+ const resolvedPath = path2__default.resolve(rootDir, manifestPath || DEFAULT_MANIFEST_FILENAME);
9609
10797
  let raw;
9610
10798
  try {
9611
10799
  raw = await readFile(resolvedPath, "utf-8");
@@ -9688,6 +10876,12 @@ function mergeManifestNavigation(analyzerNavigation, manifestNavigation) {
9688
10876
  };
9689
10877
  }
9690
10878
 
10879
+ // src/generators/checksum.ts
10880
+ function serializeForChecksum(document) {
10881
+ const { generatedAt: _generatedAt, ...content } = document;
10882
+ return JSON.stringify(content, null, 2);
10883
+ }
10884
+
9691
10885
  // src/pipeline/enrichment.ts
9692
10886
  function enrichScreenForAgent(screen) {
9693
10887
  const targets = mergeTargets([
@@ -9764,7 +10958,10 @@ function mergeTargets(targets) {
9764
10958
  for (const target of targets) {
9765
10959
  if (!target.id) continue;
9766
10960
  const existing = byId.get(target.id);
9767
- byId.set(target.id, existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target);
10961
+ byId.set(
10962
+ target.id,
10963
+ existing ? { ...target, ...existing, locator: existing.locator ?? target.locator } : target
10964
+ );
9768
10965
  }
9769
10966
  return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
9770
10967
  }
@@ -9809,7 +11006,10 @@ function synthesizeFlows(screen, targets) {
9809
11006
  intent: "destructive_action",
9810
11007
  steps: [
9811
11008
  { type: "press", target: action.id, label: action.label },
9812
- { type: "confirm", description: "Wait for native or custom confirmation before continuing" },
11009
+ {
11010
+ type: "confirm",
11011
+ description: "Wait for native or custom confirmation before continuing"
11012
+ },
9813
11013
  { type: "wait", description: describeWait(action) }
9814
11014
  ],
9815
11015
  waitPolicy: waitPolicyForAction(action),
@@ -9824,10 +11024,19 @@ function synthesizeFlows(screen, targets) {
9824
11024
  title: `Act on an item in ${collection.id}`,
9825
11025
  intent: "list_action",
9826
11026
  steps: [
9827
- { type: "choose-list-item", target: collection.id, description: "Resolve the user reference to a visible or searchable row" },
9828
- { type: "press", description: collection.rowAction?.description ?? "Open the row action" }
11027
+ {
11028
+ type: "choose-list-item",
11029
+ target: collection.id,
11030
+ description: "Resolve the user reference to a visible or searchable row"
11031
+ },
11032
+ {
11033
+ type: "press",
11034
+ description: collection.rowAction?.description ?? "Open the row action"
11035
+ }
9829
11036
  ],
9830
- waitPolicy: { expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback" }
11037
+ waitPolicy: {
11038
+ expectedOutcome: collection.rowAction?.targetScreen ? "navigation" : "inline-feedback"
11039
+ }
9831
11040
  });
9832
11041
  }
9833
11042
  }
@@ -9835,25 +11044,32 @@ function synthesizeFlows(screen, targets) {
9835
11044
  }
9836
11045
  function waitPolicyForAction(action) {
9837
11046
  const expectedOutcome = action.successSignal?.type === "goBack" ? "goBack" : action.appilotsInferred?.expectedOutcome ?? (action.targetScreen ? "navigation" : void 0);
9838
- const signals = [action.successSignal, action.failureSignal].filter(Boolean);
11047
+ const signals = [action.successSignal, action.failureSignal].filter(
11048
+ Boolean
11049
+ );
11050
+ const maxMs = action.asyncBudgetMs ?? (action.appilotsInferred?.isAsyncTrigger ? 1e4 : void 0);
9839
11051
  return {
9840
11052
  ...expectedOutcome ? { expectedOutcome } : {},
9841
11053
  ...signals && signals.length > 0 ? { signals } : {},
9842
- ...action.appilotsInferred?.isAsyncTrigger ? { maxMs: 1e4 } : {}
11054
+ ...maxMs !== void 0 ? { maxMs } : {}
9843
11055
  };
9844
11056
  }
9845
11057
  function describeWait(action) {
9846
11058
  if (action.successSignal?.description) return action.successSignal.description;
9847
- if (action.successSignal?.type === "goBack") return "Wait for the app to return to the previous screen";
11059
+ if (action.successSignal?.type === "goBack")
11060
+ return "Wait for the app to return to the previous screen";
9848
11061
  if (action.targetScreen) return `Wait for navigation to ${action.targetScreen}`;
9849
- if (action.appilotsInferred?.expectedOutcome) return `Wait for ${action.appilotsInferred.expectedOutcome}`;
11062
+ if (action.appilotsInferred?.expectedOutcome)
11063
+ return `Wait for ${action.appilotsInferred.expectedOutcome}`;
9850
11064
  return "Wait for the UI to settle";
9851
11065
  }
9852
11066
  function synthesizeAgentHints(screen, targets, flows) {
9853
11067
  const preferredTargets = targets.filter((target) => ["submit", "button", "list"].includes(target.role)).slice(0, 8).map((target) => target.id);
9854
11068
  const commonTasks = flows.slice(0, 6).map((flow) => flow.title);
9855
11069
  const safetyNotes = screen.actions.filter((action) => action.destructive || action.requiresConfirmation).map((action) => `${action.id} requires confirmation`);
9856
- const firstAsyncAction = screen.actions.find((action) => action.appilotsInferred?.isAsyncTrigger);
11070
+ const firstAsyncAction = screen.actions.find(
11071
+ (action) => action.appilotsInferred?.isAsyncTrigger || action.asyncBudgetMs !== void 0
11072
+ );
9857
11073
  const hints = {
9858
11074
  primaryGoal: synthesizePrimaryGoal(screen),
9859
11075
  commonTasks,
@@ -9880,7 +11096,9 @@ function synthesizePrimaryGoal(screen) {
9880
11096
  }
9881
11097
  const fieldCount = uniqueFields.size;
9882
11098
  if (fieldCount > 0) {
9883
- const requiredCount = Array.from(uniqueFields.values()).filter((field) => field.required).length;
11099
+ const requiredCount = Array.from(uniqueFields.values()).filter(
11100
+ (field) => field.required
11101
+ ).length;
9884
11102
  const fieldLabel = fieldCount === 1 ? "field" : "fields";
9885
11103
  goals.push(
9886
11104
  requiredCount > 0 ? `Complete a form with ${fieldCount} ${fieldLabel} (${requiredCount} required)` : `Complete a form with ${fieldCount} ${fieldLabel}`
@@ -9894,7 +11112,9 @@ function synthesizePrimaryGoal(screen) {
9894
11112
  if (submitCount > 0) {
9895
11113
  goals.push(`Submit ${submitCount === 1 ? "the primary form" : `${submitCount} forms/actions`}`);
9896
11114
  }
9897
- const asyncCount = screen.actions.filter((action) => action.appilotsInferred?.isAsyncTrigger).length;
11115
+ const asyncCount = screen.actions.filter(
11116
+ (action) => action.appilotsInferred?.isAsyncTrigger
11117
+ ).length;
9898
11118
  if (asyncCount > 0) {
9899
11119
  goals.push(`Wait for ${asyncCount === 1 ? "async feedback" : "async action feedback"}`);
9900
11120
  }
@@ -9969,7 +11189,7 @@ var MCPGenerator = class _MCPGenerator {
9969
11189
  const agentReadyScreens = mergedScreens.map(
9970
11190
  (screen) => enrichScreenForAgent({
9971
11191
  ...screen,
9972
- filePath: screen.filePath ? path8__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
11192
+ filePath: screen.filePath ? path2__default.relative(this.generatorConfig.rootDir, screen.filePath) : screen.filePath
9973
11193
  })
9974
11194
  );
9975
11195
  const projectInfo = await this.getProjectInfo();
@@ -9993,11 +11213,11 @@ var MCPGenerator = class _MCPGenerator {
9993
11213
  }
9994
11214
  };
9995
11215
  const serialized = JSON.stringify(document, null, 2);
9996
- const checksum = this.calculateChecksum(serialized);
9997
- const filePath = path8__default.resolve(outputDir, `mcp-document.${this.options.format}`);
11216
+ const checksum = this.calculateChecksum(serializeForChecksum(document));
11217
+ const filePath = path2__default.resolve(outputDir, `mcp-document.${this.options.format}`);
9998
11218
  await writeFile(filePath, serialized, "utf-8");
9999
11219
  console.log(`[MCPGenerator] Document written to: ${filePath}`);
10000
- const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
11220
+ const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
10001
11221
  await writeFile(checksumFilePath, checksum, "utf-8");
10002
11222
  console.log(`[MCPGenerator] Checksum written to: ${checksumFilePath}`);
10003
11223
  console.log("[MCPGenerator] Generation complete!");
@@ -10005,7 +11225,8 @@ var MCPGenerator = class _MCPGenerator {
10005
11225
  document,
10006
11226
  filePath,
10007
11227
  format: this.options.format,
10008
- checksum
11228
+ checksum,
11229
+ ...analyzed.diagnostics ? { diagnostics: analyzed.diagnostics } : {}
10009
11230
  };
10010
11231
  }
10011
11232
  /**
@@ -10017,7 +11238,7 @@ var MCPGenerator = class _MCPGenerator {
10017
11238
  * after calling `generate()`.
10018
11239
  */
10019
11240
  static async readPreviousChecksum(outputDir) {
10020
- const checksumFilePath = path8__default.resolve(outputDir, ".appilots-checksum");
11241
+ const checksumFilePath = path2__default.resolve(outputDir, ".appilots-checksum");
10021
11242
  try {
10022
11243
  const content = await readFile(checksumFilePath, "utf-8");
10023
11244
  return content.trim() || null;
@@ -10036,7 +11257,7 @@ var MCPGenerator = class _MCPGenerator {
10036
11257
  */
10037
11258
  async getProjectInfo() {
10038
11259
  try {
10039
- const packageJsonPath = path8__default.resolve(this.analyzerConfig.rootDir, "package.json");
11260
+ const packageJsonPath = path2__default.resolve(this.analyzerConfig.rootDir, "package.json");
10040
11261
  const packageJsonContent = await readFile(packageJsonPath, "utf-8");
10041
11262
  const packageJson = JSON.parse(packageJsonContent);
10042
11263
  return {
@@ -10119,12 +11340,12 @@ function getEnvOverrides(env = process.env) {
10119
11340
  return trimmed ? trimmed : void 0;
10120
11341
  };
10121
11342
  return {
10122
- apiKey: clean(env.APPILOTS_API_KEY),
11343
+ apiKey: clean(env.APPILOTS_PUBLISH_KEY) ?? clean(env.APPILOTS_API_KEY),
10123
11344
  projectId: clean(env.APPILOTS_PROJECT_ID),
10124
11345
  serverUrl: clean(env.APPILOTS_SERVER_URL)
10125
11346
  };
10126
11347
  }
10127
- function loadConfig(onWarn) {
11348
+ function loadConfig(onWarn, options = {}) {
10128
11349
  const configPath = getConfigPath();
10129
11350
  const env = getEnvOverrides();
10130
11351
  let fileConfig = {};
@@ -10163,6 +11384,10 @@ function loadConfig(onWarn) {
10163
11384
  };
10164
11385
  const validation = validateConfig(merged);
10165
11386
  validation.warnings.unshift(...legacyWarnings);
11387
+ if (options.requireApiKey === false) {
11388
+ validation.errors = validation.errors.filter((e) => !e.startsWith("apiKey"));
11389
+ validation.valid = validation.errors.length === 0;
11390
+ }
10166
11391
  if (fileConfig.serverUrl === void 0 && env.serverUrl === void 0) {
10167
11392
  for (const key of Object.keys(fileConfig)) {
10168
11393
  if (KNOWN_CONFIG_KEYS.includes(key)) continue;
@@ -10595,7 +11820,7 @@ var AppilotsAPIClient = class {
10595
11820
  };
10596
11821
 
10597
11822
  // src/version.ts
10598
- var CLI_VERSION = "0.11.0";
11823
+ var CLI_VERSION = "0.11.3";
10599
11824
 
10600
11825
  export { AppilotsAPIClient, CLI_VERSION, ComponentAnalyzer, DEFAULT_MANIFEST_FILENAME, DEFAULT_WEB_SCREEN_PATTERNS, FormAnalyzer, GenericPlatformAnalyzer, MCPGenerator, NavigationAnalyzer, ReactNativePlatformAnalyzer, ReactWebPlatformAnalyzer, ScreenAnalyzer, WebNavigationAnalyzer, WebScreenAnalyzer, formatMetadataWarnings, getConfigPath, getEnvOverrides, lintActionMetadata, loadConfig, loadManifest, mergeManifestNavigation, mergeManifestScreens, resolvePathToScreen, saveConfig, screenNameFromPath, validateConfig };
10601
11826
  //# sourceMappingURL=index.mjs.map