@filipebraida/adonis-function-points 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +1 -1
  3. package/build/commands/main.js +6 -6
  4. package/build/{fp_calibrate-EAuAtdbq.js → fp_calibrate-B_oD9b02.js} +1 -1
  5. package/build/{fp_count-CZ0cUUBQ.js → fp_count-B_KPNKMO.js} +1 -1
  6. package/build/{fp_diff-BTg_LX0r.js → fp_diff-B3NhXzrb.js} +1 -1
  7. package/build/{fp_explain-D6QvDLKQ.js → fp_explain-C2s6Kpaj.js} +1 -1
  8. package/build/{fp_inventory-C43fU39x.js → fp_inventory-B322MvZT.js} +1 -1
  9. package/build/{fp_metrics-DEMPk4xC.js → fp_metrics-DjADb7F6.js} +1 -1
  10. package/build/index.js +2 -2
  11. package/build/{pipeline-Cq4dNTNE.js → pipeline-C6kHKB9-.js} +2363 -114
  12. package/build/{resolvers-DlKJOZnk.js → resolvers-DhJO-qvQ.js} +339 -151
  13. package/build/{runners-FYmPIPub.js → runners-BpBJsGMj.js} +2 -2
  14. package/build/src/albrecht/counter.d.ts +12 -1
  15. package/build/src/cli.js +2 -2
  16. package/build/src/inventory/graph/call_graph.d.ts +22 -0
  17. package/build/src/inventory/graph/deliveries.d.ts +94 -0
  18. package/build/src/inventory/graph/output_fields.d.ts +45 -1
  19. package/build/src/inventory/graph/pages.d.ts +44 -0
  20. package/build/src/inventory/resolvers/index.js +1 -1
  21. package/build/src/inventory/resolvers/job_dispatch.d.ts +20 -0
  22. package/build/src/inventory/resolvers/local_function.d.ts +24 -0
  23. package/build/src/inventory/resolvers/transformer.d.ts +0 -23
  24. package/build/src/inventory/sources/commands.d.ts +14 -0
  25. package/build/src/inventory/sources/jobs.d.ts +27 -0
  26. package/build/src/pipeline.js +1 -1
  27. package/build/src/types.d.ts +23 -0
  28. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
- import { a as chainShapeOf, c as hooksFiredBy, d as isApplicationCode, f as isSeeder, h as toPosix, i as resolveCall, l as rootSymbolOf, m as samePath, o as outputFieldsIn, p as relativeTo, r as isTechnicalWrite, s as detectAccess, t as BUILTIN_CALL_RESOLVERS, u as collectEventBindings } from "./resolvers-DlKJOZnk.js";
1
+ import { _ as relativeTo, a as chainShapeOf, c as unwrap$1, d as collectEventBindings, f as detectAccess, g as isSeeder, h as isApplicationCode, i as resolveCall, l as DISPATCH_METHODS, m as rootSymbolOf, o as mappedLiteralOf, p as hooksFiredBy, r as isTechnicalWrite, s as outputFieldsIn, t as BUILTIN_CALL_RESOLVERS, u as EXECUTION_METHODS, v as samePath, y as toPosix } from "./resolvers-DhJO-qvQ.js";
2
2
  import fs from "node:fs/promises";
3
- import path from "node:path";
3
+ import path, { dirname, join, resolve } from "node:path";
4
4
  import { Node, Project, SyntaxKind } from "ts-morph";
5
5
  import { createHash } from "node:crypto";
6
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
7
  import { execFileSync } from "node:child_process";
7
- import { readFileSync } from "node:fs";
8
8
  //#region src/inventory/app_context.ts
9
9
  /** folders naming an artefact TYPE, in either layout */
10
10
  const ARTIFACT_KINDS = new Set([
@@ -965,93 +965,116 @@ const problemAt = (call, expression, reason) => ({
965
965
  reason
966
966
  });
967
967
  //#endregion
968
- //#region src/inventory/sources/json_schemas.ts
969
- function collectJsonSchemas(app) {
968
+ //#region src/inventory/sources/commands.ts
969
+ /**
970
+ * Ace commands as entry points — counting-decisions §5, plan 0.7 §C.
971
+ *
972
+ * IFPUG counts batch processes an operator starts. `node ace noticias:importar`
973
+ * reads a feed and writes news: an EI exactly like a `POST`, and until this the
974
+ * collector emitted only `kind: 'http'`, although the type had `command` and §5
975
+ * had decided its identity since 0.1.0.
976
+ *
977
+ * AdonisJS scans `./commands` for the application's own commands; so does this.
978
+ * A command is a class extending `BaseCommand` from `@adonisjs/core/ace` with a
979
+ * literal `static commandName`; its body is `run()`, followed by the same
980
+ * strategies as any handler. What it reaches decides whether it is a transaction
981
+ * at all — a scaffolder that writes files reaches no store and falls out, as a
982
+ * static route does.
983
+ */
984
+ const ACE_MODULE = "@adonisjs/core/ace";
985
+ const BASE_COMMAND = "BaseCommand";
986
+ /** a generator of test data, by the package everyone uses for it */
987
+ const FAKER = /^@faker-js\/faker/;
988
+ const GENERATES_DATA_HINT = "imports @faker-js/faker: generates data, probably a development tool";
989
+ function collectCommands(app) {
970
990
  const project = new Project({
971
991
  skipAddingFilesFromTsConfig: true,
972
992
  skipFileDependencyResolution: true,
973
993
  compilerOptions: { allowJs: false }
974
994
  });
975
- /**
976
- * Wider than `scanRoots`, and only here.
977
- *
978
- * `database/` is excluded from the application roots on purpose: test factories
979
- * and migrations contain real persistence calls, and scanning them would turn a
980
- * test write into a counted function. But `node ace make:seeder` puts seeders in
981
- * `database/seeders`, which is where seed data — and therefore a form's schema —
982
- * normally lives. Reading a literal counts nothing, so there is no conflict: the
983
- * exclusion protects the call graph, not the schema catalogue.
984
- *
985
- * Without this, `overrides.detFromSchema` naming a schema declared in a seeder
986
- * reported "not declared anywhere in the code" and left the DET count at the
987
- * floor — the exact case the override exists for.
988
- */
989
- for (const root of [...app.scanRoots.map(toPosix), `${toPosix(app.root)}/database`]) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
990
- const found = /* @__PURE__ */ new Map();
991
- for (const file of project.getSourceFiles()) for (const declaration of file.getVariableDeclarations()) {
992
- const literal = unwrap(declaration.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
993
- if (!literal || !isJsonSchema(literal)) continue;
994
- const leaves = leavesOf$1(literal);
995
- if (leaves.length === 0) continue;
996
- found.set(declaration.getName(), {
997
- name: declaration.getName(),
998
- fields: leaves.length,
999
- leaves,
1000
- provenance: {
1001
- file: toPosix(file.getFilePath()),
1002
- line: declaration.getStartLineNumber(),
1003
- by: "json-schema"
1004
- }
1005
- });
995
+ project.addSourceFilesAtPaths(`${toPosix(app.root)}/commands/**/*.ts`);
996
+ const entryPoints = [];
997
+ for (const file of project.getSourceFiles()) {
998
+ if (/\.(spec|test)\.ts$/.test(file.getBaseName())) continue;
999
+ const baseCommand = baseCommandNameIn(file);
1000
+ if (!baseCommand) continue;
1001
+ for (const cls of file.getClasses()) {
1002
+ if (cls.getExtends()?.getExpression().getText() !== baseCommand) continue;
1003
+ const name = commandNameOf(cls);
1004
+ if (!name) continue;
1005
+ const filePath = file.getFilePath();
1006
+ const hints = importsFaker(file) ? [GENERATES_DATA_HINT] : [];
1007
+ entryPoints.push({
1008
+ id: `ace ${name}`,
1009
+ kind: "command",
1010
+ module: app.moduleOf(filePath),
1011
+ trigger: "ace",
1012
+ signature: name,
1013
+ name,
1014
+ handler: {
1015
+ file: filePath,
1016
+ member: "run"
1017
+ },
1018
+ identity: `ace ${name}`,
1019
+ provenance: {
1020
+ file: filePath,
1021
+ line: cls.getStartLineNumber(),
1022
+ by: "ace-commands"
1023
+ },
1024
+ ...hints.length ? { hints } : {}
1025
+ });
1026
+ }
1006
1027
  }
1007
- return found;
1028
+ return entryPoints.sort((a, b) => a.identity.localeCompare(b.identity));
1008
1029
  }
1009
- /** `{ … } as const` and `{ … } satisfies X` still hold the literal */
1010
- function unwrap(node) {
1011
- if (!node) return void 0;
1012
- if (Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) return unwrap(node.getExpression());
1013
- return node;
1030
+ /** the local name `BaseCommand` was imported under from `@adonisjs/core/ace`, if it was */
1031
+ function baseCommandNameIn(file) {
1032
+ for (const declaration of file.getImportDeclarations()) {
1033
+ if (declaration.getModuleSpecifierValue() !== ACE_MODULE) continue;
1034
+ const named = declaration.getNamedImports().find((n) => n.getName() === BASE_COMMAND);
1035
+ if (named) return named.getAliasNode()?.getText() ?? BASE_COMMAND;
1036
+ }
1037
+ return null;
1014
1038
  }
1015
- /**
1016
- * Recognised by shape, never by name.
1017
- *
1018
- * A const called `schema` may be anything; an object declaring `type: 'object'`
1019
- * with a `properties` map is a JSON Schema whatever it is called.
1020
- */
1021
- function isJsonSchema(literal) {
1022
- return (literal.getProperty("type")?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer()?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue() === "object" && literal.getProperty("properties") !== void 0;
1039
+ /** `static commandName = 'noticias:importar'` — a literal; a computed name is nobody's identity */
1040
+ function commandNameOf(cls) {
1041
+ const property = cls.getStaticProperty("commandName");
1042
+ if (!property || !Node.isPropertyDeclaration(property)) return null;
1043
+ const initializer = property.getInitializer();
1044
+ if (!initializer) return null;
1045
+ if (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) return initializer.getLiteralValue();
1046
+ return null;
1023
1047
  }
1048
+ function importsFaker(file) {
1049
+ return file.getImportDeclarations().some((d) => FAKER.test(d.getModuleSpecifierValue()));
1050
+ }
1051
+ /** the ace decorators that declare what the operator types: `@flags.string()`, `@args.string()` */
1052
+ const INPUT_DECORATORS = new Set(["flags", "args"]);
1024
1053
  /**
1025
- * Leaves of a JSON Schema, by the table in counting-decisions §7.
1026
- *
1027
- * scalar 1
1028
- * nested object leaves counted individually
1029
- * array of scalar 1 (repeating group)
1030
- * array of object the object's leaves, once
1031
- * enum / const 1
1054
+ * The input DETs of a command: its `@flags.*` and `@args.*`, named as the
1055
+ * operator types them — `flagName` / `argumentName` when given, the property
1056
+ * otherwise. Returned as `flags.<name>` / `args.<name>` so the counter can say
1057
+ * which is which.
1032
1058
  */
1033
- function leavesOf$1(literal, prefix = "") {
1034
- const properties = literal.getProperty("properties")?.asKind(SyntaxKind.PropertyAssignment);
1035
- const items = literal.getProperty("items")?.asKind(SyntaxKind.PropertyAssignment);
1036
- if (properties) {
1037
- const map = properties.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
1038
- if (!map) return [];
1039
- return map.getProperties().flatMap((property) => {
1040
- const assignment = property.asKind(SyntaxKind.PropertyAssignment);
1041
- if (!assignment) return [];
1042
- const name = assignment.getName().replace(/['"]/g, "");
1043
- const path = prefix ? `${prefix}.${name}` : name;
1044
- const nested = unwrap(assignment.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
1045
- const inner = nested ? leavesOf$1(nested, path) : [];
1046
- return inner.length > 0 ? inner : [path];
1047
- });
1048
- }
1049
- if (items) {
1050
- const element = unwrap(items.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
1051
- const inner = element ? leavesOf$1(element, prefix) : [];
1052
- return inner.length > 0 ? inner : [prefix];
1059
+ function commandFieldsOf(cls) {
1060
+ const fields = [];
1061
+ for (const property of cls.getProperties()) for (const decorator of property.getDecorators()) {
1062
+ const call = decorator.getCallExpression();
1063
+ const callee = call?.getExpression();
1064
+ if (!call || !callee || !Node.isPropertyAccessExpression(callee)) continue;
1065
+ const kind = callee.getExpression().getText();
1066
+ if (!INPUT_DECORATORS.has(kind)) continue;
1067
+ const options = call.getArguments()[0];
1068
+ const literal = (options && Node.isObjectLiteralExpression(options) ? ["flagName", "argumentName"].map((key) => options.getProperty(key)).find((p) => p && Node.isPropertyAssignment(p)) : void 0)?.getInitializer();
1069
+ const name = literal && Node.isStringLiteral(literal) ? literal.getLiteralValue() : property.getName();
1070
+ fields.push(`${kind}.${name}`);
1053
1071
  }
1054
- return [];
1072
+ return fields;
1073
+ }
1074
+ /** is this class an ace command? — the graph asks, to read its flags as input */
1075
+ function isCommandClass(cls) {
1076
+ const baseCommand = baseCommandNameIn(cls.getSourceFile());
1077
+ return !!baseCommand && cls.getExtends()?.getExpression().getText() === baseCommand;
1055
1078
  }
1056
1079
  //#endregion
1057
1080
  //#region src/inventory/graph/noise.ts
@@ -1164,7 +1187,10 @@ const FRAMEWORK_SERVICES = new Set([
1164
1187
  "encryption",
1165
1188
  "i18n",
1166
1189
  "ally",
1167
- "bouncer"
1190
+ "bouncer",
1191
+ "ui",
1192
+ "colors",
1193
+ "app"
1168
1194
  ]);
1169
1195
  /** Is this call one that cannot reach a data store? */
1170
1196
  function isNoise(call, owner) {
@@ -1239,6 +1265,1336 @@ function isNoiseMember(file, member) {
1239
1265
  return base !== void 0 && FRAMEWORK_SERVICES.has(base);
1240
1266
  }
1241
1267
  //#endregion
1268
+ //#region src/inventory/graph/deliveries.ts
1269
+ /** `inertia.render(page, props)`, `inertia.modal(page, props)`, `view.render(view, props)` */
1270
+ const RENDERERS = new Set(["inertia", "view"]);
1271
+ const RENDER_METHODS = new Set(["render", "modal"]);
1272
+ /** `response.json(x)`, `.ok(x)`, `.created(x)`, `.send(x)`, `.accepted(x)` */
1273
+ const RESPONSE_METHODS = new Set([
1274
+ "json",
1275
+ "ok",
1276
+ "created",
1277
+ "accepted",
1278
+ "send"
1279
+ ]);
1280
+ /**
1281
+ * An ace command prints: `this.ui.table().row([…])`, `this.logger.info(…)`,
1282
+ * `console.log(…)`. What a report hands to the terminal is what leaves — every
1283
+ * argument is delivered, read like a prop (plan 0.7 §C).
1284
+ */
1285
+ const PRINTERS = new Set([
1286
+ "ui",
1287
+ "logger",
1288
+ "console"
1289
+ ]);
1290
+ /** `x.data`, `x.rows` on a paginated / wrapped result hand the collection on */
1291
+ const PASSES_THROUGH$1 = new Set([
1292
+ "data",
1293
+ "rows",
1294
+ "all",
1295
+ "toJSON",
1296
+ "serialize"
1297
+ ]);
1298
+ /** keys of a result that carry its rows: picking one of these is not picking one value */
1299
+ const PASSES_ROWS = new Set([
1300
+ "data",
1301
+ "rows",
1302
+ "items",
1303
+ "results",
1304
+ "list",
1305
+ "linhas",
1306
+ "itens"
1307
+ ]);
1308
+ /** properties of a result that are one value, not its rows */
1309
+ const SCALAR_PROPS = new Set([
1310
+ "length",
1311
+ "size",
1312
+ "total",
1313
+ "count"
1314
+ ]);
1315
+ /**
1316
+ * What Lucid's `paginator.getMeta()` says that a page can show. The URLs are
1317
+ * navigation and `firstPage` a constant: neither is a user-recognisable attribute.
1318
+ */
1319
+ const PAGINATOR_META = [
1320
+ "total",
1321
+ "perPage",
1322
+ "currentPage",
1323
+ "lastPage"
1324
+ ];
1325
+ /** methods that return the same collection, or one of its rows: what leaves is the receiver */
1326
+ const SAME_COLLECTION = new Set([
1327
+ "slice",
1328
+ "filter",
1329
+ "sort",
1330
+ "sortBy",
1331
+ "toSorted",
1332
+ "reverse",
1333
+ "toReversed",
1334
+ "concat",
1335
+ "flat",
1336
+ "find",
1337
+ "findLast",
1338
+ "at",
1339
+ "first",
1340
+ "last"
1341
+ ]);
1342
+ /** reads of the request whose result echoes input already counted on entry */
1343
+ const ECHOES_INPUT = new Set([
1344
+ "validateUsing",
1345
+ "input",
1346
+ "only",
1347
+ "all",
1348
+ "body",
1349
+ "qs",
1350
+ "params"
1351
+ ]);
1352
+ /** Inertia's lazy props: `inertia.defer(() => q.handle())` — the callback's value is what leaves */
1353
+ const INERTIA_LAZY = new Set([
1354
+ "defer",
1355
+ "lazy",
1356
+ "optional",
1357
+ "always",
1358
+ "merge",
1359
+ "scroll",
1360
+ "once"
1361
+ ]);
1362
+ /** a yes/no: an authorisation check, a membership test */
1363
+ const BOOLEAN_METHODS = new Set([
1364
+ "allows",
1365
+ "denies",
1366
+ "can",
1367
+ "cannot",
1368
+ "includes",
1369
+ "has",
1370
+ "startsWith",
1371
+ "endsWith",
1372
+ "test"
1373
+ ]);
1374
+ /** a value formatted: still one value */
1375
+ const FORMAT_METHODS = new Set([
1376
+ "join",
1377
+ "toString",
1378
+ "toISO",
1379
+ "toISODate",
1380
+ "toISOTime",
1381
+ "toISOString",
1382
+ "toFormat",
1383
+ "toRFC2822",
1384
+ "toHTTP",
1385
+ "toSQL",
1386
+ "toLocaleString",
1387
+ "toLocaleDateString",
1388
+ "toFixed",
1389
+ "toUnixInteger",
1390
+ "toMillis",
1391
+ "trim",
1392
+ "toUpperCase",
1393
+ "toLowerCase",
1394
+ "padStart",
1395
+ "padEnd",
1396
+ "replace",
1397
+ "slice",
1398
+ "substring"
1399
+ ]);
1400
+ /** framework services whose calls hand back one value: a translation, a session key, a URL */
1401
+ const SCALAR_SERVICES = new Set([
1402
+ "i18n",
1403
+ "session",
1404
+ "env",
1405
+ "router",
1406
+ "encryption",
1407
+ "hash",
1408
+ "config",
1409
+ "app"
1410
+ ]);
1411
+ /** built-ins whose static calls are one value or a constant list: `Object.values(Enum)`, `JSON.stringify(x)` */
1412
+ const NATIVE_GLOBALS = new Set([
1413
+ "Object",
1414
+ "Array",
1415
+ "JSON",
1416
+ "Math",
1417
+ "Number",
1418
+ "String",
1419
+ "Boolean",
1420
+ "Date",
1421
+ "Intl"
1422
+ ]);
1423
+ /**
1424
+ * The deliveries of a body: every props argument handed to a renderer or a
1425
+ * response method, and what a `return` hands back that is not one of those calls.
1426
+ */
1427
+ function deliveriesIn(ctx) {
1428
+ const deliveries = [];
1429
+ const returns = [];
1430
+ let any = false;
1431
+ let anyReturn = false;
1432
+ const seen = /* @__PURE__ */ new Set();
1433
+ for (const call of ctx.body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
1434
+ const callee = call.getExpression();
1435
+ if (!Node.isPropertyAccessExpression(callee)) continue;
1436
+ const method = callee.getName();
1437
+ const receiver = lastSegmentOf(callee.getExpression());
1438
+ let payload;
1439
+ let via;
1440
+ if (RENDERERS.has(receiver) && RENDER_METHODS.has(method)) {
1441
+ payload = call.getArguments()[1];
1442
+ const name = unwrap$1(call.getArguments()[0]);
1443
+ if (name && Node.isStringLiteral(name)) via = {
1444
+ engine: receiver === "view" ? "edge" : "inertia",
1445
+ page: name.getLiteralValue()
1446
+ };
1447
+ } else if (receiver === "response" && RESPONSE_METHODS.has(method)) payload = call.getArguments()[0];
1448
+ else if (isPrinter(callee.getExpression(), ctx.body)) {
1449
+ any = true;
1450
+ seen.add(call);
1451
+ for (const argument of call.getArguments()) classify(unwrap$1(argument), "", ctx, deliveries, 0);
1452
+ continue;
1453
+ } else continue;
1454
+ any = true;
1455
+ if (!payload) continue;
1456
+ seen.add(call);
1457
+ const before = deliveries.length;
1458
+ classify(unwrap$1(payload), "", ctx, deliveries, 0);
1459
+ if (via) for (let i = before; i < deliveries.length; i++) deliveries[i].via = via;
1460
+ }
1461
+ for (const statement of ctx.body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
1462
+ if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== ctx.body) continue;
1463
+ const value = unwrap$1(statement.getExpression());
1464
+ if (!value) continue;
1465
+ if (Node.isCallExpression(value)) {
1466
+ if (seen.has(value)) continue;
1467
+ /**
1468
+ * `return response.redirect().toRoute(…)`, `return response.noContent()`,
1469
+ * `return inertia.render(…)` (seen above): a call whose chain is rooted at
1470
+ * the response or a renderer is the response itself, not a value handed
1471
+ * back — nothing to classify.
1472
+ */
1473
+ const root = chainRootOf(value);
1474
+ if (root && (RENDERERS.has(root) || root === "response")) continue;
1475
+ }
1476
+ anyReturn = true;
1477
+ classify(value, "", ctx, returns, 0);
1478
+ }
1479
+ return {
1480
+ calls: deliveries,
1481
+ anyCall: any,
1482
+ returns,
1483
+ anyReturn
1484
+ };
1485
+ }
1486
+ /** the identifier a call chain is rooted at: `response.redirect().toRoute(x)` -> 'response' */
1487
+ function chainRootOf(node) {
1488
+ let current = node;
1489
+ for (let depth = 0; current && depth < 40; depth++) {
1490
+ if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
1491
+ current = current.getExpression();
1492
+ continue;
1493
+ }
1494
+ if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current)) {
1495
+ current = current.getExpression();
1496
+ continue;
1497
+ }
1498
+ return Node.isIdentifier(current) ? current.getText() : Node.isThisExpression(current) ? "this" : null;
1499
+ }
1500
+ return null;
1501
+ }
1502
+ /**
1503
+ * `this.ui.table().row(x)`, `this.logger.info(x)`, `console.log(x)`: a chain rooted
1504
+ * at a printer — through a variable too (`const table = this.ui.table(); table.row(x)`).
1505
+ */
1506
+ function isPrinter(receiver, body, depth = 0) {
1507
+ let current = receiver;
1508
+ for (let steps = 0; current && steps < 20; steps++) {
1509
+ if (Node.isCallExpression(current)) {
1510
+ current = current.getExpression();
1511
+ continue;
1512
+ }
1513
+ if (Node.isPropertyAccessExpression(current)) {
1514
+ const inner = current.getExpression();
1515
+ if (Node.isThisExpression(inner)) return PRINTERS.has(current.getName());
1516
+ current = inner;
1517
+ continue;
1518
+ }
1519
+ if (!Node.isIdentifier(current)) return false;
1520
+ if (current.getText() === "console") return true;
1521
+ const bound = depth < 3 ? bindingOf(current.getText(), body) : null;
1522
+ return !!bound && isPrinter(bound.initializer, body, depth + 1);
1523
+ }
1524
+ return false;
1525
+ }
1526
+ /** `ctx.inertia` -> 'inertia', `inertia` -> 'inertia', `this.response` -> 'response' */
1527
+ function lastSegmentOf(node) {
1528
+ if (Node.isPropertyAccessExpression(node)) return node.getName();
1529
+ return node.getText();
1530
+ }
1531
+ function classify(value, path, ctx, out, depth) {
1532
+ if (!value || depth > 6) return;
1533
+ if (value.getKind() === SyntaxKind.NullKeyword) return;
1534
+ if (Node.isIdentifier(value) && value.getText() === "undefined") return;
1535
+ /**
1536
+ * A constant with no key — `this.logger.info('done')`, a table's `head(['Nome'])`
1537
+ * — is a label or a message, not a field: AFP counts no message DET (§6). Keyed,
1538
+ * `{ titulo: 'X' }` is a value the page receives, and stays one.
1539
+ */
1540
+ if (!path && (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) || Node.isNumericLiteral(value))) return;
1541
+ if (Node.isObjectLiteralExpression(value)) {
1542
+ for (const property of value.getProperties()) if (Node.isShorthandPropertyAssignment(property)) classify(property.getNameNode(), join$1(path, property.getName()), ctx, out, depth + 1);
1543
+ else if (Node.isPropertyAssignment(property)) {
1544
+ const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
1545
+ classify(unwrap$1(property.getInitializer()), join$1(path, name), ctx, out, depth + 1);
1546
+ } else if (Node.isSpreadAssignment(property)) classify(unwrap$1(property.getExpression()), path, ctx, out, depth + 1);
1547
+ else out.push({
1548
+ kind: "scalar",
1549
+ path: join$1(path, property.getName?.() ?? "*")
1550
+ });
1551
+ return;
1552
+ }
1553
+ if (Node.isConditionalExpression(value)) {
1554
+ classify(unwrap$1(value.getWhenTrue()), path, ctx, out, depth + 1);
1555
+ classify(unwrap$1(value.getWhenFalse()), path, ctx, out, depth + 1);
1556
+ return;
1557
+ }
1558
+ if (Node.isArrayLiteralExpression(value)) {
1559
+ for (const element of value.getElements()) classify(unwrap$1(Node.isSpreadElement(element) ? element.getExpression() : element), path, ctx, out, depth + 1);
1560
+ return;
1561
+ }
1562
+ if (Node.isElementAccessExpression(value)) {
1563
+ classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
1564
+ return;
1565
+ }
1566
+ if (Node.isIdentifier(value)) {
1567
+ const name = value.getText();
1568
+ if (ctx.symbols.has(name)) {
1569
+ out.push({
1570
+ kind: "store",
1571
+ store: ctx.symbols.get(name),
1572
+ path
1573
+ });
1574
+ return;
1575
+ }
1576
+ const bound = bindingOf(name, ctx.body);
1577
+ if (bound) {
1578
+ if (bound.pick && Node.isCallExpression(bound.initializer)) {
1579
+ classifyCall(bound.initializer, path, ctx, out, depth + 1, bound.pick);
1580
+ return;
1581
+ }
1582
+ if (bound.pick && Node.isObjectLiteralExpression(bound.initializer)) {
1583
+ const picked = bound.initializer.getProperty(bound.pick);
1584
+ if (picked && Node.isPropertyAssignment(picked)) {
1585
+ classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
1586
+ return;
1587
+ }
1588
+ if (picked && Node.isShorthandPropertyAssignment(picked)) {
1589
+ classify(picked.getNameNode(), path, ctx, out, depth + 1);
1590
+ return;
1591
+ }
1592
+ }
1593
+ classify(bound.initializer, path, ctx, out, depth + 1);
1594
+ return;
1595
+ }
1596
+ if (isEchoBinding(name, ctx.body)) {
1597
+ out.push({
1598
+ kind: "echo",
1599
+ path
1600
+ });
1601
+ return;
1602
+ }
1603
+ out.push({
1604
+ kind: "scalar",
1605
+ path
1606
+ });
1607
+ return;
1608
+ }
1609
+ if (Node.isCallExpression(value)) {
1610
+ classifyCall(value, path, ctx, out, depth);
1611
+ return;
1612
+ }
1613
+ if (Node.isPropertyAccessExpression(value) || Node.isElementAccessExpression(value)) {
1614
+ classifyAccess(value, path, ctx, out, depth);
1615
+ return;
1616
+ }
1617
+ if (Node.isAwaitExpression(value)) {
1618
+ classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
1619
+ return;
1620
+ }
1621
+ if (Node.isBinaryExpression(value)) {
1622
+ const operator = value.getOperatorToken().getKind();
1623
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
1624
+ classify(unwrap$1(value.getLeft()), path, ctx, out, depth + 1);
1625
+ return;
1626
+ }
1627
+ if (operator === SyntaxKind.AmpersandAmpersandToken) {
1628
+ classify(unwrap$1(value.getRight()), path, ctx, out, depth + 1);
1629
+ return;
1630
+ }
1631
+ }
1632
+ if (Node.isTemplateExpression(value)) {
1633
+ for (const span of value.getTemplateSpans()) classify(unwrap$1(span.getExpression()), path, ctx, out, depth + 1);
1634
+ return;
1635
+ }
1636
+ out.push({
1637
+ kind: "scalar",
1638
+ path
1639
+ });
1640
+ }
1641
+ function classifyCall(value, path, ctx, out, depth, pick) {
1642
+ {
1643
+ const callee = value.getExpression();
1644
+ const method = Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
1645
+ if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(method) && isRequestLike(callee.getExpression())) {
1646
+ out.push({
1647
+ kind: "echo",
1648
+ path
1649
+ });
1650
+ return;
1651
+ }
1652
+ const mapped = mappedLiteralOf(value);
1653
+ if (mapped) {
1654
+ classify(mapped, path, ctx, out, depth + 1);
1655
+ return;
1656
+ }
1657
+ if (method === "map" && Node.isPropertyAccessExpression(callee)) {
1658
+ classifyMapped(value, callee.getExpression(), path, ctx, out, depth);
1659
+ return;
1660
+ }
1661
+ if (Node.isPropertyAccessExpression(callee) && PASSES_THROUGH$1.has(method) && value.getArguments().length === 0) {
1662
+ classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
1663
+ return;
1664
+ }
1665
+ if (method === "getMeta" && value.getArguments().length === 0) {
1666
+ if (pick) out.push({
1667
+ kind: "scalar",
1668
+ path
1669
+ });
1670
+ else for (const key of PAGINATOR_META) out.push({
1671
+ kind: "scalar",
1672
+ path: join$1(path, key)
1673
+ });
1674
+ return;
1675
+ }
1676
+ if (Node.isPropertyAccessExpression(callee) && SAME_COLLECTION.has(method)) {
1677
+ classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
1678
+ return;
1679
+ }
1680
+ if (Node.isPropertyAccessExpression(callee) && lastSegmentOf(callee.getExpression()) === "colors") {
1681
+ const coloured = unwrap$1(value.getArguments()[0]);
1682
+ if (coloured) classify(coloured, path, ctx, out, depth + 1);
1683
+ return;
1684
+ }
1685
+ if (Node.isPropertyAccessExpression(callee) && INERTIA_LAZY.has(method) && RENDERERS.has(lastSegmentOf(callee.getExpression()))) {
1686
+ const body = callbackValueOf(unwrap$1(value.getArguments()[0]));
1687
+ if (body) classify(body, path, ctx, out, depth + 1);
1688
+ else out.push({
1689
+ kind: "scalar",
1690
+ path
1691
+ });
1692
+ return;
1693
+ }
1694
+ const args = [];
1695
+ for (const argument of value.getArguments()) classify(unwrap$1(argument), path, ctx, args, depth + 1);
1696
+ /** only what carries rows matters for a fallback: a scalar argument is a parameter, not an output */
1697
+ const carried = args.filter((item) => item.kind === "store" || item.kind === "call");
1698
+ if (Node.isPropertyAccessExpression(callee)) {
1699
+ const root = chainRootOf(callee.getExpression());
1700
+ if (root && SCALAR_SERVICES.has(root)) {
1701
+ out.push({
1702
+ kind: "scalar",
1703
+ path
1704
+ });
1705
+ return;
1706
+ }
1707
+ }
1708
+ const refs = ctx.followed.get(value);
1709
+ if (refs && refs.length > 0) {
1710
+ out.push({
1711
+ kind: "call",
1712
+ refs,
1713
+ path,
1714
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60),
1715
+ args: carried,
1716
+ ...pick ? { pick } : {}
1717
+ });
1718
+ return;
1719
+ }
1720
+ const access = detectAccess(value, ctx.symbols, ctx.relations);
1721
+ if (access) {
1722
+ out.push({
1723
+ kind: "store",
1724
+ store: access.store,
1725
+ path
1726
+ });
1727
+ if (access.viaRelation) out.push({
1728
+ kind: "store",
1729
+ store: access.viaRelation,
1730
+ path
1731
+ });
1732
+ return;
1733
+ }
1734
+ if (Node.isIdentifier(callee) && NATIVE_GLOBALS.has(callee.getText())) {
1735
+ out.push({
1736
+ kind: "scalar",
1737
+ path
1738
+ });
1739
+ return;
1740
+ }
1741
+ if (Node.isPropertyAccessExpression(callee) && Node.isIdentifier(callee.getExpression()) && NATIVE_GLOBALS.has(callee.getExpression().getText())) {
1742
+ out.push({
1743
+ kind: "scalar",
1744
+ path
1745
+ });
1746
+ return;
1747
+ }
1748
+ /**
1749
+ * A call nobody followed, over rows: a package's CSV builder, a formatter. The
1750
+ * document it builds carries what was handed into it, so the rows' stores leave.
1751
+ * With nothing flowing in there is nothing to say: one DET, opaque, reported.
1752
+ */
1753
+ if (carried.length > 0) {
1754
+ out.push(...carried);
1755
+ return;
1756
+ }
1757
+ /**
1758
+ * `startDate?.toISOString()`, `categoria.trim()`: a call ON a value the body
1759
+ * holds is that value formatted — an echo stays an echo, a store's field one
1760
+ * field. Only when the root itself resolves to nothing else is the call read
1761
+ * on its own.
1762
+ */
1763
+ if (Node.isPropertyAccessExpression(callee)) {
1764
+ const root = chainRootOf(callee.getExpression());
1765
+ if (root && ctx.symbols.has(root)) {
1766
+ if (readsField(callee.getExpression())) out.push({
1767
+ kind: "scalar",
1768
+ path
1769
+ });
1770
+ else out.push({
1771
+ kind: "store",
1772
+ store: ctx.symbols.get(root),
1773
+ path
1774
+ });
1775
+ return;
1776
+ }
1777
+ if (root && root !== "this") {
1778
+ const rootNode = rootIdentifierOf(callee.getExpression());
1779
+ const held = [];
1780
+ if (rootNode) classify(rootNode, path, ctx, held, depth + 1);
1781
+ if (held.length > 0 && held.every((item) => item.kind === "echo")) {
1782
+ out.push({
1783
+ kind: "echo",
1784
+ path
1785
+ });
1786
+ return;
1787
+ }
1788
+ }
1789
+ }
1790
+ /**
1791
+ * `rows.map(…).join('\n')` in a body whose parameter is `rows`: the whole input
1792
+ * transformed, so the input says what leaves — the caller's arguments decide,
1793
+ * and the body itself says nothing. `noticia.publicadaEm?.toISO()` on that
1794
+ * parameter is one FIELD of it, and stays one value below.
1795
+ */
1796
+ if (Node.isPropertyAccessExpression(callee) && transformsParameter(callee, ctx.body)) {
1797
+ out.push({
1798
+ kind: "opaque",
1799
+ path,
1800
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60)
1801
+ });
1802
+ return;
1803
+ }
1804
+ if (Node.isPropertyAccessExpression(callee)) {
1805
+ /**
1806
+ * `comunicado.enviadoEm!.toISODate()`, `(a ?? b).toRFC2822()`: a FIELD read off
1807
+ * the chain, or an expression, then a method — one value. A chain rooted at
1808
+ * `this` (`this.service.find()`) is a call into a service, and not this.
1809
+ */
1810
+ if (readsField(callee.getExpression())) {
1811
+ out.push({
1812
+ kind: "scalar",
1813
+ path
1814
+ });
1815
+ return;
1816
+ }
1817
+ if (BOOLEAN_METHODS.has(method) || FORMAT_METHODS.has(method)) {
1818
+ out.push({
1819
+ kind: "scalar",
1820
+ path
1821
+ });
1822
+ return;
1823
+ }
1824
+ }
1825
+ if (returnsPrimitive(value)) {
1826
+ out.push({
1827
+ kind: "scalar",
1828
+ path
1829
+ });
1830
+ return;
1831
+ }
1832
+ out.push({
1833
+ kind: "opaque",
1834
+ path,
1835
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60)
1836
+ });
1837
+ }
1838
+ }
1839
+ /**
1840
+ * `xs.map(cb)` with no literal in the callback: what the callback RETURNS,
1841
+ * once — a function passed by reference (`rows.map(paraLinha)`) is a call to
1842
+ * that function over the rows; an expression body (`(m) => new T(m).toObject()`)
1843
+ * is classified as if it were the value; anything else is one repeating attribute.
1844
+ */
1845
+ function classifyMapped(value, receiver, path, ctx, out, depth) {
1846
+ const callback = unwrap$1(value.getArguments()[0]);
1847
+ const refs = ctx.followed.get(value);
1848
+ if (callback && Node.isIdentifier(callback) && refs && refs.length > 0) {
1849
+ const rows = [];
1850
+ classify(unwrap$1(receiver), path, ctx, rows, depth + 1);
1851
+ out.push({
1852
+ kind: "call",
1853
+ refs,
1854
+ path,
1855
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60),
1856
+ args: rows.filter((item) => item.kind === "store" || item.kind === "call")
1857
+ });
1858
+ return;
1859
+ }
1860
+ const body = callbackValueOf(callback);
1861
+ if (body) {
1862
+ classify(body, path, ctx, out, depth + 1);
1863
+ return;
1864
+ }
1865
+ out.push({
1866
+ kind: "scalar",
1867
+ path
1868
+ });
1869
+ }
1870
+ /** the value a callback hands back: an expression body, or the one `return` of a block */
1871
+ function callbackValueOf(callback) {
1872
+ if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return null;
1873
+ const body = callback.getBody();
1874
+ if (!Node.isBlock(body)) return unwrap$1(body);
1875
+ const returns = body.getStatements().filter(Node.isReturnStatement);
1876
+ return returns.length === 1 ? unwrap$1(returns[0].getExpression()) : null;
1877
+ }
1878
+ /**
1879
+ * Does this chain read a property (not a method) or hold an expression before
1880
+ * the method is applied? `comunicado.enviadoEm.toISO()` does; `rows.map(f).join()`
1881
+ * does not; a chain rooted at `this` is a service, and does not.
1882
+ */
1883
+ function readsField(node) {
1884
+ let current = node;
1885
+ for (let depth = 0; current && depth < 40; depth++) {
1886
+ if (Node.isCallExpression(current)) {
1887
+ const inner = current.getExpression();
1888
+ current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
1889
+ continue;
1890
+ }
1891
+ if (Node.isAwaitExpression(current) || Node.isNonNullExpression(current)) {
1892
+ current = current.getExpression();
1893
+ continue;
1894
+ }
1895
+ if (Node.isParenthesizedExpression(current)) {
1896
+ const inner = unwrap$1(current.getExpression());
1897
+ if (!inner) return false;
1898
+ if (Node.isBinaryExpression(inner) || Node.isConditionalExpression(inner)) return true;
1899
+ current = inner;
1900
+ continue;
1901
+ }
1902
+ if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return chainRootOf(current) !== "this";
1903
+ return false;
1904
+ }
1905
+ return false;
1906
+ }
1907
+ /**
1908
+ * Is this chain a method (or methods) applied to a plain parameter of the body,
1909
+ * with no field read in between? `rows.map(f).join(s)` is; `noticia.capa?.toISO()`
1910
+ * reads a field first and is not.
1911
+ */
1912
+ function transformsParameter(callee, body) {
1913
+ let current = callee.getExpression();
1914
+ for (let depth = 0; current && depth < 40; depth++) {
1915
+ if (Node.isCallExpression(current)) {
1916
+ const inner = current.getExpression();
1917
+ current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
1918
+ continue;
1919
+ }
1920
+ if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
1921
+ current = current.getExpression();
1922
+ continue;
1923
+ }
1924
+ if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return false;
1925
+ if (!Node.isIdentifier(current)) return false;
1926
+ const name = current.getText();
1927
+ return Node.isParametered(body) && body.getParameters().some((p) => Node.isIdentifier(p.getNameNode()) && p.getNameNode().getText() === name);
1928
+ }
1929
+ return false;
1930
+ }
1931
+ /** the identifier node a chain is rooted at: `startDate?.toISOString` -> `startDate` */
1932
+ function rootIdentifierOf(node) {
1933
+ let current = node;
1934
+ for (let depth = 0; current && depth < 40; depth++) {
1935
+ if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
1936
+ current = current.getExpression();
1937
+ continue;
1938
+ }
1939
+ return Node.isIdentifier(current) ? current : null;
1940
+ }
1941
+ return null;
1942
+ }
1943
+ /** the call's declared return type, `Promise<…>` unwrapped, is a boolean, string, number or a union of those */
1944
+ function returnsPrimitive(call) {
1945
+ try {
1946
+ let type = call.getReturnType();
1947
+ if (type.getSymbol()?.getName() === "Promise") type = type.getTypeArguments()[0] ?? type;
1948
+ const primitive = (t) => t.isBoolean() || t.isBooleanLiteral() || t.isString() || t.isStringLiteral() || t.isNumber() || t.isNumberLiteral() || t.isEnumLiteral() || t.isNull() || t.isUndefined();
1949
+ if (type.isUnion()) {
1950
+ const members = type.getUnionTypes();
1951
+ return members.length > 0 && members.every(primitive) && !members.every((t) => t.isNull() || t.isUndefined());
1952
+ }
1953
+ return primitive(type);
1954
+ } catch {
1955
+ return false;
1956
+ }
1957
+ }
1958
+ /**
1959
+ * `x.data`, `resultado.linhas`, `rows[0]`: a part of a value the body holds.
1960
+ *
1961
+ * on a variable bound to a followed call that key of what the call returns
1962
+ * on a store-bound variable the store (`rows[0]`, `.data`) or a field (scalar)
1963
+ * on the validated payload or the request an echo of input — counts on entry
1964
+ * anything else one value
1965
+ */
1966
+ function classifyAccess(value, path, ctx, out, depth) {
1967
+ const root = rootSymbolOf(value);
1968
+ /**
1969
+ * `x.nome`, `x['nome']`: the key named; `x[papel]`: a key the body computes — one
1970
+ * value, unnamed (`*`); `rows[0]`: one row of the collection, the collection.
1971
+ */
1972
+ const property = Node.isPropertyAccessExpression(value) ? value.getName() : keyOfElementAccess(value);
1973
+ if (root && ctx.symbols.has(root)) {
1974
+ if (!property || PASSES_THROUGH$1.has(property)) out.push({
1975
+ kind: "store",
1976
+ store: ctx.symbols.get(root),
1977
+ path
1978
+ });
1979
+ else out.push({
1980
+ kind: "scalar",
1981
+ path: path || property.replace(/^\*$/, "")
1982
+ });
1983
+ return;
1984
+ }
1985
+ if (root) {
1986
+ const bound = bindingOf(root, ctx.body);
1987
+ if (bound && Node.isCallExpression(bound.initializer)) {
1988
+ if (SCALAR_PROPS.has(property)) {
1989
+ out.push({
1990
+ kind: "scalar",
1991
+ path
1992
+ });
1993
+ return;
1994
+ }
1995
+ /**
1996
+ * `resultado.linhas`: one key of what the call returns; `meta.pagina` on a
1997
+ * destructured `meta`: the key under the key; `rows[0]`, `.data`: the whole.
1998
+ */
1999
+ const own = property && !PASSES_THROUGH$1.has(property) ? property : void 0;
2000
+ const pick = [bound.pick, own].filter(Boolean).join(".") || void 0;
2001
+ if (own && !ctx.followed.has(bound.initializer)) {
2002
+ out.push({
2003
+ kind: "scalar",
2004
+ path
2005
+ });
2006
+ return;
2007
+ }
2008
+ classifyCall(bound.initializer, path, ctx, out, depth + 1, pick);
2009
+ return;
2010
+ }
2011
+ if (bound && Node.isObjectLiteralExpression(bound.initializer) && property) {
2012
+ const picked = bound.initializer.getProperty(property);
2013
+ if (picked && Node.isPropertyAssignment(picked)) {
2014
+ classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
2015
+ return;
2016
+ }
2017
+ }
2018
+ if (isEchoBinding(root, ctx.body) || root === "request" || root === "params") {
2019
+ out.push({
2020
+ kind: "echo",
2021
+ path
2022
+ });
2023
+ return;
2024
+ }
2025
+ }
2026
+ out.push({
2027
+ kind: "scalar",
2028
+ path: path || property.replace(/^\*$/, "")
2029
+ });
2030
+ }
2031
+ function keyOfElementAccess(value) {
2032
+ const argument = unwrap$1(value.getArgumentExpression());
2033
+ if (!argument || Node.isNumericLiteral(argument)) return "";
2034
+ if (Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument)) return argument.getLiteralValue();
2035
+ return "*";
2036
+ }
2037
+ const join$1 = (prefix, name) => prefix ? `${prefix}.${name}` : name;
2038
+ /** `await x` binds x: the value, not the promise */
2039
+ function unwrapAwait$1(node) {
2040
+ let current = unwrap$1(node);
2041
+ while (current && Node.isAwaitExpression(current)) current = unwrap$1(current.getExpression());
2042
+ return current;
2043
+ }
2044
+ /**
2045
+ * How a local name was bound in this body: `const x = …` gives the initializer;
2046
+ * `const { x, y } = …` gives the initializer and the key picked out of it.
2047
+ */
2048
+ function bindingOf(name, body) {
2049
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
2050
+ const nameNode = declaration.getNameNode();
2051
+ if (Node.isIdentifier(nameNode)) {
2052
+ if (nameNode.getText() !== name) continue;
2053
+ const initializer = unwrapAwait$1(declaration.getInitializer());
2054
+ return initializer ? { initializer } : null;
2055
+ }
2056
+ if (Node.isObjectBindingPattern(nameNode)) {
2057
+ const element = nameNode.getElements().find((e) => e.getName() === name);
2058
+ if (!element) continue;
2059
+ const initializer = unwrapAwait$1(declaration.getInitializer());
2060
+ if (!initializer) return null;
2061
+ return {
2062
+ initializer,
2063
+ pick: element.getPropertyNameNode()?.getText() ?? element.getName()
2064
+ };
2065
+ }
2066
+ }
2067
+ return null;
2068
+ }
2069
+ /**
2070
+ * A name bound by destructuring the validated payload or the request:
2071
+ * `const { busca } = await request.validateUsing(x)`, `const { id } = params`.
2072
+ */
2073
+ function isEchoBinding(name, body) {
2074
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
2075
+ const binding = declaration.getNameNode();
2076
+ if (!Node.isObjectBindingPattern(binding)) continue;
2077
+ if (!binding.getElements().some((element) => element.getName() === name)) continue;
2078
+ const initializer = unwrap$1(declaration.getInitializer());
2079
+ if (!initializer) continue;
2080
+ if (Node.isIdentifier(initializer) && /^(params|request)$/.test(initializer.getText())) return true;
2081
+ if (Node.isCallExpression(initializer)) {
2082
+ const callee = initializer.getExpression();
2083
+ if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(callee.getName())) return true;
2084
+ }
2085
+ }
2086
+ return false;
2087
+ }
2088
+ const isRequestLike = (node) => lastSegmentOf(node) === "request";
2089
+ //#endregion
2090
+ //#region src/inventory/graph/pages.ts
2091
+ /** array methods whose callback receives one row */
2092
+ const ITERATES_ROWS$1 = new Set([
2093
+ "map",
2094
+ "forEach",
2095
+ "filter",
2096
+ "find",
2097
+ "findLast",
2098
+ "some",
2099
+ "every",
2100
+ "flatMap"
2101
+ ]);
2102
+ /** array methods that hand back one row, or the same rows */
2103
+ const SAME_ROWS = new Set([
2104
+ "filter",
2105
+ "slice",
2106
+ "sort",
2107
+ "toSorted",
2108
+ "reverse",
2109
+ "concat",
2110
+ "flat",
2111
+ "find",
2112
+ "findLast",
2113
+ "at"
2114
+ ]);
2115
+ /** wrappers whose property hands the rows on */
2116
+ const PASSES_THROUGH = new Set([
2117
+ "data",
2118
+ "rows",
2119
+ "all"
2120
+ ]);
2121
+ /** how deep the reader follows a row into child components */
2122
+ const MAX_COMPONENT_DEPTH = 1;
2123
+ function readPages(deliveries, env) {
2124
+ const reading = {
2125
+ columns: /* @__PURE__ */ new Map(),
2126
+ unreadable: /* @__PURE__ */ new Map(),
2127
+ unknownMembers: /* @__PURE__ */ new Map()
2128
+ };
2129
+ if (deliveries.length === 0) return reading;
2130
+ const byPage = /* @__PURE__ */ new Map();
2131
+ for (const delivery of deliveries) {
2132
+ const key = `${delivery.engine}:${delivery.page}`;
2133
+ byPage.set(key, [...byPage.get(key) ?? [], delivery]);
2134
+ }
2135
+ for (const group of byPage.values()) {
2136
+ const { engine, page } = group[0];
2137
+ const candidates = engine === "inertia" ? inertiaPageFiles(env.root, page) : edgeFiles(env.root, page);
2138
+ if (candidates.length !== 1) {
2139
+ const reason = candidates.length === 0 ? `page "${page}" not found under the ${engine === "inertia" ? "pages" : "views"} conventions` : `two files answer to the page "${page}": ${candidates.map((c) => toPosix(c).replace(`${toPosix(env.root)}/`, "")).join(", ")}`;
2140
+ for (const delivery of group) markUnreadable(reading, delivery.store, reason);
2141
+ continue;
2142
+ }
2143
+ /**
2144
+ * One prop carrying several stores — a query object whose unreadable result
2145
+ * made every store it read leave under `atuacao` — cannot be told apart by the
2146
+ * page: `atuacao.total` belongs to none of them. Whole, all of them, and said so.
2147
+ */
2148
+ const byPath = /* @__PURE__ */ new Map();
2149
+ for (const d of group) byPath.set(d.path, new Set([...byPath.get(d.path) ?? [], d.store]));
2150
+ const readable = group.filter((d) => {
2151
+ const stores = byPath.get(d.path);
2152
+ if (stores.size === 1) return true;
2153
+ markUnreadable(reading, d.store, `page "${page}": prop \`${d.path || "(props)"}\` carries ${stores.size} stores (${[...stores].join(", ")}) — what the page reads off it belongs to no one of them`);
2154
+ return false;
2155
+ });
2156
+ if (readable.length === 0) continue;
2157
+ const reader = new PageReader(env, reading);
2158
+ if (engine === "inertia") reader.readTsx(candidates[0], readable);
2159
+ else reader.readEdge(candidates[0], readable);
2160
+ /**
2161
+ * The page was read and no column of the store came out of it: either the page
2162
+ * never touches the rows, or it reads members that are not columns — a row the
2163
+ * controller serialised on the way, or computed getters. Whole, and said so.
2164
+ */
2165
+ for (const { store } of readable) {
2166
+ if (reading.columns.has(store) || reading.unreadable.has(store)) continue;
2167
+ const members = [...reading.unknownMembers.get(store) ?? []];
2168
+ markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `page "${page}" never reads ${store}`);
2169
+ }
2170
+ }
2171
+ return reading;
2172
+ }
2173
+ function markUnreadable(reading, store, reason) {
2174
+ if (!reading.unreadable.has(store)) reading.unreadable.set(store, reason);
2175
+ }
2176
+ const SKIPPED_DIRS = new Set([
2177
+ "node_modules",
2178
+ "dist",
2179
+ "build",
2180
+ ".git",
2181
+ "coverage",
2182
+ "tmp",
2183
+ ".adonisjs"
2184
+ ]);
2185
+ /**
2186
+ * `inertia.render('livros/index')` → `inertia/pages/livros/index.tsx` (the default),
2187
+ * `app/<first>/ui/pages/<rest>.tsx` (a domain-module layout), or any `pages/` directory
2188
+ * under the root holding that path. The `resolve` function of the front-end is not
2189
+ * run — a convention is read, a function is not.
2190
+ */
2191
+ function inertiaPageFiles(root, page) {
2192
+ const found = /* @__PURE__ */ new Set();
2193
+ const [first, ...rest] = page.split("/");
2194
+ if (rest.length > 0) for (const extension of [
2195
+ ".tsx",
2196
+ ".jsx",
2197
+ ".vue",
2198
+ ".svelte"
2199
+ ]) {
2200
+ const file = join(root, "app", first, "ui", "pages", `${rest.join("/")}${extension}`);
2201
+ if (existsSync(file)) found.add(file);
2202
+ }
2203
+ for (const dir of pagesDirectories(root)) for (const extension of [
2204
+ ".tsx",
2205
+ ".jsx",
2206
+ ".vue",
2207
+ ".svelte"
2208
+ ]) {
2209
+ const file = join(dir, `${page}${extension}`);
2210
+ if (existsSync(file)) found.add(file);
2211
+ const index = join(dir, page, `index${extension}`);
2212
+ if (existsSync(index)) found.add(index);
2213
+ }
2214
+ return [...found].sort();
2215
+ }
2216
+ let pagesDirsCache = /* @__PURE__ */ new Map();
2217
+ function pagesDirectories(root) {
2218
+ const cached = pagesDirsCache.get(root);
2219
+ if (cached) return cached;
2220
+ const dirs = [];
2221
+ const walk = (dir, depth) => {
2222
+ if (depth > 6) return;
2223
+ let entries;
2224
+ try {
2225
+ entries = readdirSync(dir);
2226
+ } catch {
2227
+ return;
2228
+ }
2229
+ for (const entry of entries) {
2230
+ if (SKIPPED_DIRS.has(entry) || entry.startsWith(".")) continue;
2231
+ const full = join(dir, entry);
2232
+ let isDirectory = false;
2233
+ try {
2234
+ isDirectory = statSync(full).isDirectory();
2235
+ } catch {
2236
+ continue;
2237
+ }
2238
+ if (!isDirectory) continue;
2239
+ if (entry === "pages") dirs.push(full);
2240
+ else walk(full, depth + 1);
2241
+ }
2242
+ };
2243
+ walk(root, 0);
2244
+ pagesDirsCache.set(root, dirs);
2245
+ return dirs;
2246
+ }
2247
+ /** `view.render('catalogo')` → `resources/views/catalogo.edge` */
2248
+ function edgeFiles(root, page) {
2249
+ const file = join(root, "resources", "views", `${page.replace(/\./g, "/")}.edge`);
2250
+ return existsSync(file) ? [file] : [];
2251
+ }
2252
+ /** tsconfig `paths` of the application, for the page's own imports (`~/components/x`) */
2253
+ function aliasesOf(root) {
2254
+ const aliases = /* @__PURE__ */ new Map();
2255
+ for (const config of [join(root, "tsconfig.json"), join(root, "inertia", "tsconfig.json")]) {
2256
+ if (!existsSync(config)) continue;
2257
+ try {
2258
+ const text = readFileSync(config, "utf8").replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, "");
2259
+ const paths = JSON.parse(text)?.compilerOptions?.paths;
2260
+ for (const [alias, targets] of Object.entries(paths ?? {})) {
2261
+ const target = targets[0];
2262
+ if (!target) continue;
2263
+ aliases.set(alias.replace(/\*$/, ""), resolve(dirname(config), target.replace(/\*$/, "")));
2264
+ }
2265
+ } catch {}
2266
+ }
2267
+ if (!aliases.has("~/")) aliases.set("~/", join(root, "inertia"));
2268
+ return aliases;
2269
+ }
2270
+ var PageReader = class {
2271
+ aliases;
2272
+ constructor(env, reading) {
2273
+ this.env = env;
2274
+ this.reading = reading;
2275
+ this.aliases = aliasesOf(env.root);
2276
+ }
2277
+ readTsx(file, deliveries) {
2278
+ const source = this.env.project.addSourceFileAtPathIfExists(file);
2279
+ const component = source ? defaultComponentOf(source) : null;
2280
+ if (!source || !component) {
2281
+ for (const d of deliveries) this.fail(d.store, `page "${deliveries[0].page}" has no default-exported component the reader can open`);
2282
+ return;
2283
+ }
2284
+ const scope = this.bindProps(component, deliveries.map((d) => ({
2285
+ name: d.path.split(".")[0],
2286
+ entity: {
2287
+ store: d.store,
2288
+ rest: d.path.split(".").slice(1)
2289
+ }
2290
+ })));
2291
+ this.readComponent(component, scope, 0, deliveries[0].page);
2292
+ }
2293
+ /** the component's first parameter: `{ livros }`, `{ livros: rows }`, or `props` */
2294
+ bindProps(component, props) {
2295
+ const scope = /* @__PURE__ */ new Map();
2296
+ if (!Node.isParametered(component)) return scope;
2297
+ const parameter = component.getParameters()[0];
2298
+ if (!parameter) return scope;
2299
+ const nameNode = parameter.getNameNode();
2300
+ if (Node.isObjectBindingPattern(nameNode)) for (const element of nameNode.getElements()) {
2301
+ const property = element.getPropertyNameNode()?.getText() ?? element.getName();
2302
+ const prop = props.find((p) => p.name === property);
2303
+ if (prop) scope.set(element.getName(), prop.entity);
2304
+ }
2305
+ else if (Node.isIdentifier(nameNode)) for (const prop of props) scope.set(`${nameNode.getText()}.${prop.name}`, prop.entity);
2306
+ return scope;
2307
+ }
2308
+ readComponent(component, scope, depth, page) {
2309
+ const body = Node.isParametered(component) && "getBody" in component ? component.getBody() ?? component : component;
2310
+ const file = component.getSourceFile();
2311
+ /** the entity an expression holds, recording a column when the expression IS one */
2312
+ const entityOf = (node, visiting = 0) => {
2313
+ if (!node || visiting > 12) return null;
2314
+ let current = node;
2315
+ while (Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current) || Node.isAsExpression(current) || Node.isAwaitExpression(current)) current = current.getExpression();
2316
+ if (Node.isIdentifier(current)) return scope.get(current.getText()) ?? null;
2317
+ if (Node.isPropertyAccessExpression(current)) {
2318
+ const receiver = current.getExpression();
2319
+ if (Node.isIdentifier(receiver)) {
2320
+ const asPath = scope.get(`${receiver.getText()}.${current.getName()}`);
2321
+ if (asPath) return asPath;
2322
+ }
2323
+ const holder = entityOf(receiver, visiting + 1);
2324
+ if (!holder) return null;
2325
+ return this.member(holder, current.getName());
2326
+ }
2327
+ if (Node.isElementAccessExpression(current)) {
2328
+ const holder = entityOf(current.getExpression(), visiting + 1);
2329
+ if (!holder) return null;
2330
+ const argument = current.getArgumentExpression();
2331
+ if (argument && Node.isStringLiteral(argument)) return this.member(holder, argument.getLiteralValue());
2332
+ return holder;
2333
+ }
2334
+ if (Node.isCallExpression(current)) {
2335
+ const callee = current.getExpression();
2336
+ if (Node.isPropertyAccessExpression(callee)) {
2337
+ const holder = entityOf(callee.getExpression(), visiting + 1);
2338
+ if (holder && holder.rest.length === 0 && SAME_ROWS.has(callee.getName())) return holder;
2339
+ if (holder && holder.rest.length === 0 && ITERATES_ROWS$1.has(callee.getName())) return null;
2340
+ if (holder && holder.rest.length === 0 && callee.getName() === "length") return null;
2341
+ if (holder) return null;
2342
+ }
2343
+ return null;
2344
+ }
2345
+ if (Node.isConditionalExpression(current)) {
2346
+ entityOf(current.getCondition(), visiting + 1);
2347
+ return entityOf(current.getWhenTrue(), visiting + 1) ?? entityOf(current.getWhenFalse(), visiting + 1);
2348
+ }
2349
+ if (Node.isBinaryExpression(current)) {
2350
+ const operator = current.getOperatorToken().getKind();
2351
+ if (operator === SyntaxKind.AmpersandAmpersandToken) {
2352
+ entityOf(current.getLeft(), visiting + 1);
2353
+ return entityOf(current.getRight(), visiting + 1);
2354
+ }
2355
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) return entityOf(current.getLeft(), visiting + 1) ?? entityOf(current.getRight(), visiting + 1);
2356
+ entityOf(current.getLeft(), visiting + 1);
2357
+ entityOf(current.getRight(), visiting + 1);
2358
+ return null;
2359
+ }
2360
+ if (Node.isTemplateExpression(current)) {
2361
+ for (const span of current.getTemplateSpans()) entityOf(span.getExpression(), visiting + 1);
2362
+ return null;
2363
+ }
2364
+ return null;
2365
+ };
2366
+ const escape = (entity, reason) => {
2367
+ const store = entity.rest.length === 0 ? entity.store : entity.store;
2368
+ this.fail(store, `page "${page}": ${reason}`);
2369
+ };
2370
+ body.forEachDescendant((node) => {
2371
+ if (Node.isVariableDeclaration(node)) {
2372
+ const initializer = node.getInitializer();
2373
+ const nameNode = node.getNameNode();
2374
+ if (!initializer) return;
2375
+ const entity = entityOf(initializer);
2376
+ if (!entity) return;
2377
+ if (Node.isIdentifier(nameNode)) scope.set(nameNode.getText(), entity);
2378
+ else if (Node.isObjectBindingPattern(nameNode)) for (const element of nameNode.getElements()) {
2379
+ const property = element.getPropertyNameNode()?.getText() ?? element.getName();
2380
+ const member = this.member(entity, property);
2381
+ if (member) scope.set(element.getName(), member);
2382
+ }
2383
+ return;
2384
+ }
2385
+ if (Node.isCallExpression(node)) {
2386
+ const callee = node.getExpression();
2387
+ if (Node.isPropertyAccessExpression(callee) && ITERATES_ROWS$1.has(callee.getName())) {
2388
+ const holder = entityOf(callee.getExpression());
2389
+ const callback = node.getArguments()[0];
2390
+ if (holder && holder.rest.length === 0 && callback && (Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) {
2391
+ const parameter = callback.getParameters()[0]?.getNameNode();
2392
+ if (parameter && Node.isIdentifier(parameter)) scope.set(parameter.getText(), holder);
2393
+ }
2394
+ return;
2395
+ }
2396
+ for (const argument of node.getArguments()) {
2397
+ const entity = entityOf(argument);
2398
+ if (entity && !Node.isPropertyAccessExpression(unwrapParens(argument))) escape(entity, `\`${node.getExpression().getText()}(…)\` receives the rows and the reader cannot follow a function`);
2399
+ else if (entity) escape(entity, `\`${node.getExpression().getText()}(…)\` receives the rows and the reader cannot follow a function`);
2400
+ }
2401
+ return;
2402
+ }
2403
+ if (Node.isForOfStatement(node)) {
2404
+ const declared = node.getInitializer();
2405
+ const entity = entityOf(node.getExpression());
2406
+ if (entity && Node.isVariableDeclarationList(declared)) {
2407
+ const nameNode = declared.getDeclarations()[0]?.getNameNode();
2408
+ if (nameNode && Node.isIdentifier(nameNode)) scope.set(nameNode.getText(), entity);
2409
+ }
2410
+ return;
2411
+ }
2412
+ if (Node.isSpreadAssignment(node) || Node.isJsxSpreadAttribute(node) || Node.isSpreadElement(node)) {
2413
+ const entity = entityOf(node.getExpression());
2414
+ if (entity) escape(entity, "a spread hands the whole row on");
2415
+ return;
2416
+ }
2417
+ if (Node.isJsxExpression(node)) {
2418
+ const expression = node.getExpression();
2419
+ const parent = node.getParent();
2420
+ if (!expression || Node.isJsxAttribute(parent)) return;
2421
+ const entity = entityOf(expression);
2422
+ if (entity) escape(entity, `\`{${expression.getText().replace(/\s+/g, " ").slice(0, 60)}}\` renders the row itself`);
2423
+ return;
2424
+ }
2425
+ if (Node.isJsxAttribute(node)) {
2426
+ const initializer = node.getInitializer();
2427
+ const entity = entityOf(initializer && Node.isJsxExpression(initializer) ? initializer.getExpression() : void 0);
2428
+ if (!entity) return;
2429
+ const element = node.getFirstAncestor((n) => Node.isJsxOpeningElement(n) || Node.isJsxSelfClosingElement(n));
2430
+ const tag = element && (Node.isJsxOpeningElement(element) || Node.isJsxSelfClosingElement(element)) ? element.getTagNameNode().getText() : "";
2431
+ if (!/^[A-Z]/.test(tag)) return;
2432
+ if (depth >= MAX_COMPONENT_DEPTH) {
2433
+ escape(entity, `\`<${tag} ${node.getNameNode().getText()}={…} />\` is a second level of components; the reader follows one`);
2434
+ return;
2435
+ }
2436
+ const child = this.componentOf(tag, file);
2437
+ if (!child) {
2438
+ escape(entity, `\`<${tag} />\` is not a component of the application the reader can open`);
2439
+ return;
2440
+ }
2441
+ const childScope = this.bindProps(child, [{
2442
+ name: node.getNameNode().getText(),
2443
+ entity
2444
+ }]);
2445
+ this.readComponent(child, childScope, depth + 1, page);
2446
+ return;
2447
+ }
2448
+ if (Node.isReturnStatement(node) && depth === 0) {
2449
+ const expression = node.getExpression();
2450
+ if (expression && !Node.isJsxElement(expression) && !Node.isJsxFragment(expression) && !Node.isJsxSelfClosingElement(expression) && !Node.isParenthesizedExpression(expression)) {
2451
+ const entity = entityOf(expression);
2452
+ if (entity) escape(entity, `\`return ${expression.getText().replace(/\s+/g, " ").slice(0, 60)}\` hands the rows back as they came`);
2453
+ }
2454
+ }
2455
+ });
2456
+ }
2457
+ /** `entity.name`: one column (recorded), a relation (the target), a wrapper key, or nothing */
2458
+ member(entity, name) {
2459
+ if (entity.rest.length > 0) return entity.rest[0] === name ? {
2460
+ store: entity.store,
2461
+ rest: entity.rest.slice(1)
2462
+ } : null;
2463
+ if (PASSES_THROUGH.has(name)) return entity;
2464
+ if (this.env.stores.get(entity.store)?.attributes.some((a) => a.name === name)) {
2465
+ this.column(entity.store, name);
2466
+ return null;
2467
+ }
2468
+ const target = this.env.relations.get(entity.store)?.[name];
2469
+ if (target) return {
2470
+ store: target,
2471
+ rest: []
2472
+ };
2473
+ if (![
2474
+ "length",
2475
+ "id",
2476
+ "map",
2477
+ "filter",
2478
+ "find",
2479
+ "some",
2480
+ "every",
2481
+ "forEach",
2482
+ "slice",
2483
+ "sort"
2484
+ ].includes(name)) {
2485
+ const members = this.reading.unknownMembers.get(entity.store) ?? /* @__PURE__ */ new Set();
2486
+ members.add(name);
2487
+ this.reading.unknownMembers.set(entity.store, members);
2488
+ }
2489
+ return null;
2490
+ }
2491
+ /** the component a JSX tag names, in this file or through its imports (tsconfig paths, relative) */
2492
+ componentOf(tag, file) {
2493
+ const local = file.getFunction(tag) ?? functionVariable(file, tag);
2494
+ if (local) return local;
2495
+ for (const declaration of file.getImportDeclarations()) {
2496
+ if (![declaration.getDefaultImport()?.getText(), ...declaration.getNamedImports().map((n) => n.getAliasNode()?.getText() ?? n.getName())].includes(tag)) continue;
2497
+ const target = this.resolveImport(declaration.getModuleSpecifierValue(), file.getFilePath());
2498
+ if (!target) return null;
2499
+ const source = this.env.project.addSourceFileAtPathIfExists(target);
2500
+ if (!source) return null;
2501
+ const exported = declaration.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === tag)?.getName() ?? tag;
2502
+ return source.getFunction(exported) ?? functionVariable(source, exported) ?? (declaration.getDefaultImport()?.getText() === tag ? defaultComponentOf(source) : null);
2503
+ }
2504
+ return null;
2505
+ }
2506
+ resolveImport(specifier, from) {
2507
+ let base = this.env.resolveSpecifier(specifier);
2508
+ if (base) base = base.replace(/\.(ts|tsx|js|jsx)$/, "");
2509
+ if (!base && specifier.startsWith(".")) base = resolve(dirname(from), specifier);
2510
+ else for (const [alias, dir] of this.aliases) if (specifier.startsWith(alias)) base = join(dir, specifier.slice(alias.length));
2511
+ if (!base) return null;
2512
+ for (const candidate of [
2513
+ base,
2514
+ `${base}.tsx`,
2515
+ `${base}.ts`,
2516
+ `${base}.jsx`,
2517
+ join(base, "index.tsx"),
2518
+ join(base, "index.ts")
2519
+ ]) if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
2520
+ return null;
2521
+ }
2522
+ readEdge(file, deliveries, depth = 0, scope) {
2523
+ const text = readFileSync(file, "utf8");
2524
+ const page = deliveries[0].page;
2525
+ const bound = scope ?? /* @__PURE__ */ new Map();
2526
+ if (!scope) for (const d of deliveries) bound.set(d.path.split(".")[0], {
2527
+ store: d.store,
2528
+ rest: d.path.split(".").slice(1)
2529
+ });
2530
+ const entityOf = (expression) => {
2531
+ const parts = expression.trim().replace(/\?\./g, ".").split(".");
2532
+ let entity = bound.get(parts[0]) ?? null;
2533
+ for (const part of parts.slice(1)) {
2534
+ if (!entity) return null;
2535
+ entity = this.member(entity, part.replace(/\(.*$/, ""));
2536
+ }
2537
+ return entity;
2538
+ };
2539
+ const read = (expression) => {
2540
+ for (const chain of expression.match(/[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)+/g) ?? []) {
2541
+ const entity = entityOf(chain);
2542
+ if (entity) this.fail(entity.store, `page "${page}": \`${chain}\` is a row handed on, not a column`);
2543
+ }
2544
+ const bare = expression.trim();
2545
+ if (/^[A-Za-z_$][\w$]*$/.test(bare) && bound.has(bare)) this.fail(bound.get(bare).store, `page "${page}": \`{{ ${bare} }}\` renders the rows themselves`);
2546
+ };
2547
+ for (const match of text.matchAll(/@each\(\s*\(?\s*([A-Za-z_$][\w$]*)(?:\s*,\s*[A-Za-z_$][\w$]*)?\s*\)?\s+in\s+([^)]+)\)/g)) {
2548
+ const entity = entityOf(match[2]);
2549
+ if (entity) bound.set(match[1], entity);
2550
+ }
2551
+ for (const match of text.matchAll(/\{\{\{?\s*([\s\S]*?)\s*\}?\}\}/g)) read(match[1]);
2552
+ for (const match of text.matchAll(/@(?:if|elseif|unless)\(([^)]*)\)/g)) read(match[1]);
2553
+ for (const match of text.matchAll(/@!?(?:include|component|layout)\(\s*['"]([^'"]+)['"]/g)) {
2554
+ const included = join(this.env.root, "resources", "views", `${match[1].replace(/\./g, "/")}.edge`);
2555
+ if (!existsSync(included)) continue;
2556
+ if (depth >= MAX_COMPONENT_DEPTH) {
2557
+ for (const entity of bound.values()) this.fail(entity.store, `page "${page}": \`@${match[0].slice(1).split("(")[0]}('${match[1]}')\` is a second level; the reader follows one`);
2558
+ continue;
2559
+ }
2560
+ this.readEdge(included, deliveries, depth + 1, new Map(bound));
2561
+ }
2562
+ }
2563
+ column(store, name) {
2564
+ const columns = this.reading.columns.get(store) ?? /* @__PURE__ */ new Set();
2565
+ columns.add(name);
2566
+ this.reading.columns.set(store, columns);
2567
+ }
2568
+ fail(store, reason) {
2569
+ markUnreadable(this.reading, store, reason);
2570
+ }
2571
+ };
2572
+ function unwrapParens(node) {
2573
+ let current = node;
2574
+ while (Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) current = current.getExpression();
2575
+ return current;
2576
+ }
2577
+ /** `export default function Page(…)`, or `export default Page` naming a function or an arrow */
2578
+ function defaultComponentOf(file) {
2579
+ const declared = file.getFunctions().find((f) => f.isDefaultExport());
2580
+ if (declared) return declared;
2581
+ for (const assignment of file.getExportAssignments()) {
2582
+ const expression = assignment.getExpression();
2583
+ if (Node.isIdentifier(expression)) return file.getFunction(expression.getText()) ?? functionVariable(file, expression.getText());
2584
+ if (Node.isArrowFunction(expression) || Node.isFunctionExpression(expression)) return expression;
2585
+ if (Node.isCallExpression(expression)) {
2586
+ const inner = expression.getArguments()[0];
2587
+ if (inner && Node.isIdentifier(inner)) return file.getFunction(inner.getText()) ?? functionVariable(file, inner.getText());
2588
+ }
2589
+ }
2590
+ return null;
2591
+ }
2592
+ /** `const Page = (props) => …` / `function (…) {}` */
2593
+ function functionVariable(file, name) {
2594
+ const initializer = file.getVariableDeclaration(name)?.getInitializer();
2595
+ return initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer)) ? initializer : null;
2596
+ }
2597
+ //#endregion
1242
2598
  //#region src/inventory/graph/call_graph.ts
1243
2599
  /**
1244
2600
  * Fields declared by the validators used in this body.
@@ -1271,7 +2627,7 @@ function validatorFieldsIn(body, file, app) {
1271
2627
  * validator and is usually not exported, so looking for it where the call site
1272
2628
  * is finds nothing — which is how five fields stayed invisible.
1273
2629
  */
1274
- const { leaves, opaque: unreadable } = leavesOf(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
2630
+ const { leaves, opaque: unreadable } = leavesOf$1(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
1275
2631
  const isOpaque = new Set(unreadable);
1276
2632
  for (const leaf of leaves) {
1277
2633
  const field = `${name}.${leaf}`;
@@ -1414,7 +2770,7 @@ function calleeOwner(call) {
1414
2770
  const owner = callee.getExpression();
1415
2771
  return Node.isPropertyAccessExpression(owner) ? owner.getName() : void 0;
1416
2772
  }
1417
- function leavesOf(node, resolveRef) {
2773
+ function leavesOf$1(node, resolveRef) {
1418
2774
  const object = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
1419
2775
  if (!object) return {
1420
2776
  leaves: [],
@@ -1593,23 +2949,196 @@ function createAnalyzer(app, stores, options = {}) {
1593
2949
  factsCache.set(key, facts);
1594
2950
  return facts;
1595
2951
  };
2952
+ /** store name by the posix path of the model that declares it */
2953
+ const storeByFile = /* @__PURE__ */ new Map();
2954
+ for (const [name, store] of storesByName) storeByFile.set(toPosix(store.provenance.file), name);
2955
+ /**
2956
+ * The guard's user: `auth.user`, `auth.getUserOrFail()` are rows of the model
2957
+ * `config/auth.ts` names in its provider (`model: () => import('#models/user')`).
2958
+ * Read, never assumed — an application with no such config binds nothing.
2959
+ */
2960
+ const authUserStore = (() => {
2961
+ const config = project.addSourceFileAtPathIfExists(`${toPosix(app.root)}/config/auth.ts`);
2962
+ if (!config) return null;
2963
+ for (const call of config.getDescendantsOfKind(SyntaxKind.CallExpression)) {
2964
+ if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
2965
+ if (call.getFirstAncestorByKind(SyntaxKind.PropertyAssignment)?.getName() !== "model") continue;
2966
+ const specifier = call.getArguments()[0];
2967
+ if (!specifier || !Node.isStringLiteral(specifier)) continue;
2968
+ const target = app.resolveSpecifier(specifier.getLiteralValue());
2969
+ const store = target ? storeByFile.get(toPosix(target)) : void 0;
2970
+ if (store) return store;
2971
+ }
2972
+ return null;
2973
+ })();
2974
+ /**
2975
+ * What a body's locals ARE — plan 0.8 §A. `storeSymbolsFor` binds what the
2976
+ * parameters and the static imports say; this pass, run in source order before
2977
+ * the accesses are detected, binds what the body's own statements say:
2978
+ *
2979
+ * const s = await this.sessoes.ativa(d) the followed body's return type,
2980
+ * or its returns when unannotated
2981
+ * const n = id ? await N.find(id) : new N() a conditional, both branches N
2982
+ * const p = (await P.first()) ?? new P() a default, both operands P
2983
+ * const u = auth.getUserOrFail() the guard's model, per config/auth.ts
2984
+ * const pasta = documento.pasta a relation read off a loaded row
2985
+ * for (const d of pasta.documentos) a row of the relation's target
2986
+ * rows.map(async (d) => …) the callback's parameter, a row
2987
+ *
2988
+ * Every one is a reading of what the code declares; none is a guess. A `save()`
2989
+ * on a local none of this can type is reported as unresolved — not dropped.
2990
+ */
2991
+ function bindLocals(body, symbols, context, strategies) {
2992
+ const returnedStoreOf = (call, from = context, depth = 0) => {
2993
+ if (depth > 2) return null;
2994
+ const resolved = resolveCall(call, from, strategies);
2995
+ if (!resolved || resolved.refs.length === 0) return null;
2996
+ const found = /* @__PURE__ */ new Set();
2997
+ for (const target of resolved.refs) {
2998
+ const source = sourceFile(target.file);
2999
+ const resolvedBody = source ? findBody(source, target) : null;
3000
+ if (!resolvedBody) continue;
3001
+ const annotation = Node.isReturnTyped(resolvedBody) ? resolvedBody.getReturnTypeNode()?.getText() : void 0;
3002
+ if (annotation) {
3003
+ const store = storeNamedBy(annotation, storesByName);
3004
+ if (store) found.add(store);
3005
+ else return null;
3006
+ continue;
3007
+ }
3008
+ const own = storeSymbolsFor(resolvedBody, source, app, storesByName, relationsByStore);
3009
+ const returns = resolvedBody.getDescendantsOfKind(SyntaxKind.ReturnStatement).filter((r) => r.getFirstAncestor((n) => Node.isMethodDeclaration(n) || Node.isFunctionDeclaration(n) || Node.isArrowFunction(n) || Node.isFunctionExpression(n)) === resolvedBody);
3010
+ if (returns.length === 0) return null;
3011
+ for (const statement of returns) {
3012
+ const value = statement.getExpression();
3013
+ if (!value) return null;
3014
+ const unwrapped = unwrapAwait(value);
3015
+ if (unwrapped.getKind() === SyntaxKind.NullKeyword) continue;
3016
+ if (Node.isCallExpression(unwrapped) && !detectAccess(unwrapped, own, relationsByStore)) {
3017
+ const store = returnedStoreOf(unwrapped, {
3018
+ ...context,
3019
+ file: source,
3020
+ ...importsFor(source),
3021
+ injected: injectedFor(resolvedBody.getFirstAncestorByKind(SyntaxKind.ClassDeclaration), source, app)
3022
+ }, depth + 1);
3023
+ if (!store) return null;
3024
+ found.add(store);
3025
+ continue;
3026
+ }
3027
+ const store = Node.isNewExpression(unwrapped) ? storesByName.has(unwrapped.getExpression().getText()) ? unwrapped.getExpression().getText() : null : own.get(rootSymbolOf(unwrapped) ?? "") ?? (storesByName.has(rootSymbolOf(unwrapped) ?? "") ? rootSymbolOf(unwrapped) : null);
3028
+ if (!store) return null;
3029
+ found.add(store);
3030
+ }
3031
+ }
3032
+ return found.size === 1 ? [...found][0] : null;
3033
+ };
3034
+ const isAuthUser = (node) => {
3035
+ if (!authUserStore) return false;
3036
+ const chain = Node.isCallExpression(node) ? node.getExpression() : node;
3037
+ if (!Node.isPropertyAccessExpression(chain)) return false;
3038
+ if (rootSymbolOf(chain) !== "auth" && lastSegmentText(chain.getExpression()) !== "auth") return false;
3039
+ const last = chain.getName();
3040
+ return last === "user" || last === "getUserOrFail" || last === "authenticate";
3041
+ };
3042
+ /** the store a value is rows (or one row) of, by what the code says — or nothing */
3043
+ const storeOfValue = (value, depth = 0) => {
3044
+ if (!value || depth > 6) return null;
3045
+ const node = unwrapAwait(value);
3046
+ if (node.getKind() === SyntaxKind.NullKeyword) return null;
3047
+ if (Node.isIdentifier(node)) {
3048
+ if (node.getText() === "undefined") return null;
3049
+ return symbols.get(node.getText()) ?? null;
3050
+ }
3051
+ if (Node.isNewExpression(node)) {
3052
+ const name = node.getExpression().getText();
3053
+ return storesByName.has(name) ? name : null;
3054
+ }
3055
+ if (Node.isConditionalExpression(node)) return sameStore(storeOfValue(node.getWhenTrue(), depth + 1), storeOfValue(node.getWhenFalse(), depth + 1), node.getWhenTrue(), node.getWhenFalse());
3056
+ if (Node.isBinaryExpression(node)) {
3057
+ const operator = node.getOperatorToken().getKind();
3058
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) return sameStore(storeOfValue(node.getLeft(), depth + 1), storeOfValue(node.getRight(), depth + 1), node.getLeft(), node.getRight());
3059
+ return null;
3060
+ }
3061
+ if (Node.isElementAccessExpression(node)) return storeOfValue(node.getExpression(), depth + 1);
3062
+ if (isAuthUser(node)) return authUserStore;
3063
+ if (Node.isPropertyAccessExpression(node)) return storeOfExpression(node, symbols, relationsByStore);
3064
+ if (Node.isCallExpression(node)) {
3065
+ const access = detectAccess(node, symbols, relationsByStore);
3066
+ if (access) return access.method === "related" && access.viaRelation ? access.viaRelation : access.store;
3067
+ const callee = node.getExpression();
3068
+ if (Node.isPropertyAccessExpression(callee) && ONE_OF_ROWS.has(callee.getName())) return storeOfValue(callee.getExpression(), depth + 1);
3069
+ return returnedStoreOf(node);
3070
+ }
3071
+ return null;
3072
+ };
3073
+ const isNullish = (node) => {
3074
+ const inner = unwrapAwait(node);
3075
+ return inner.getKind() === SyntaxKind.NullKeyword || Node.isIdentifier(inner) && inner.getText() === "undefined";
3076
+ };
3077
+ const sameStore = (a, b, left, right) => {
3078
+ if (a && b) return a === b ? a : null;
3079
+ if (a && isNullish(right)) return a;
3080
+ if (b && isNullish(left)) return b;
3081
+ return null;
3082
+ };
3083
+ body.forEachDescendant((node) => {
3084
+ if (Node.isVariableDeclaration(node)) {
3085
+ const initializer = node.getInitializer();
3086
+ const nameNode = node.getNameNode();
3087
+ if (!initializer) return;
3088
+ if (Node.isIdentifier(nameNode)) {
3089
+ if (symbols.has(nameNode.getText())) return;
3090
+ const store = storeOfValue(initializer);
3091
+ if (store) symbols.set(nameNode.getText(), store);
3092
+ }
3093
+ return;
3094
+ }
3095
+ if (Node.isForOfStatement(node)) {
3096
+ const declared = node.getInitializer();
3097
+ if (!Node.isVariableDeclarationList(declared)) return;
3098
+ const nameNode = declared.getDeclarations()[0]?.getNameNode();
3099
+ if (!nameNode || !Node.isIdentifier(nameNode) || symbols.has(nameNode.getText())) return;
3100
+ const store = storeOfValue(node.getExpression());
3101
+ if (store) symbols.set(nameNode.getText(), store);
3102
+ return;
3103
+ }
3104
+ if (Node.isCallExpression(node)) {
3105
+ const callee = node.getExpression();
3106
+ if (!Node.isPropertyAccessExpression(callee) || !ITERATES_ROWS.has(callee.getName())) return;
3107
+ const callback = node.getArguments()[0];
3108
+ if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return;
3109
+ const parameter = callback.getParameters()[0]?.getNameNode();
3110
+ if (!parameter || !Node.isIdentifier(parameter) || symbols.has(parameter.getText())) return;
3111
+ const store = storeOfValue(callee.getExpression());
3112
+ if (store) symbols.set(parameter.getText(), store);
3113
+ }
3114
+ });
3115
+ }
1596
3116
  function computeFacts(ref) {
1597
3117
  const file = sourceFile(ref.file);
1598
3118
  if (!file) return null;
1599
3119
  const body = findBody(file, ref);
1600
3120
  if (!body) return null;
1601
3121
  const { imports, exportedAs } = importsFor(file);
3122
+ /** locals imported from packages, not from the application: their objects are not stores */
3123
+ const packageImports = /* @__PURE__ */ new Set();
3124
+ for (const declaration of file.getImportDeclarations()) {
3125
+ if (app.resolveSpecifier(declaration.getModuleSpecifierValue())) continue;
3126
+ const defaultImport = declaration.getDefaultImport()?.getText();
3127
+ if (defaultImport) packageImports.add(defaultImport);
3128
+ for (const named of declaration.getNamedImports()) packageImports.add(named.getAliasNode()?.getText() ?? named.getName());
3129
+ }
1602
3130
  /** the class this body belongs to: how `this.something` resolves */
1603
3131
  const owner = body.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
1604
3132
  const injected = injectedFor(owner, file, app);
1605
- const symbols = storeSymbolsFor(body, file, app, storesByName);
3133
+ const symbols = storeSymbolsFor(body, file, app, storesByName, relationsByStore);
1606
3134
  const accesses = [];
1607
3135
  const followUps = [];
1608
3136
  const unresolved = [];
1609
3137
  const reads = [];
1610
- /** calls a strategy claimed: a nested transformer's keys arrive through its body */
1611
- const followedCalls = /* @__PURE__ */ new Set();
3138
+ /** calls a strategy claimed, and where they lead: a nested transformer's keys arrive through its body */
3139
+ const followedCalls = /* @__PURE__ */ new Map();
1612
3140
  const validator = validatorFieldsIn(body, file, app);
3141
+ const commandFields = owner && isCommandClass(owner) ? commandFieldsOf(owner) : [];
1613
3142
  const request = requestFieldsIn(body);
1614
3143
  const context = {
1615
3144
  file,
@@ -1622,6 +3151,7 @@ function createAnalyzer(app, stores, options = {}) {
1622
3151
  resolveSpecifier: app.resolveSpecifier,
1623
3152
  sourceFile
1624
3153
  };
3154
+ bindLocals(body, symbols, context, resolvers);
1625
3155
  for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
1626
3156
  const access = detectAccess(call, symbols, relationsByStore);
1627
3157
  if (access) {
@@ -1652,10 +3182,15 @@ function createAnalyzer(app, stores, options = {}) {
1652
3182
  if (access.mode === "read") {
1653
3183
  const chain = chainShapeOf(call);
1654
3184
  const shape = chain.aggregate ? "aggregate" : chain.selected.length > 0 ? "select" : "whole";
3185
+ /**
3186
+ * The select list survives an aggregate shape: `select('categoria').count()`
3187
+ * with a GROUP BY leaves the grouped column as well as the count. Dropping
3188
+ * it left a summary at 1 DET.
3189
+ */
1655
3190
  const read = (store, how, via) => reads.push({
1656
3191
  store,
1657
3192
  shape: how,
1658
- columns: how === "select" ? chain.selected : [],
3193
+ columns: how === "whole" ? [] : chain.selected,
1659
3194
  ...via ? { via } : {}
1660
3195
  });
1661
3196
  if (access.method === "related" && access.viaRelation) read(access.viaRelation, shape);
@@ -1709,7 +3244,22 @@ function createAnalyzer(app, stores, options = {}) {
1709
3244
  by: resolved.by,
1710
3245
  technical
1711
3246
  });
1712
- followedCalls.add(call);
3247
+ followedCalls.set(call, resolved.refs);
3248
+ continue;
3249
+ }
3250
+ /**
3251
+ * `alvo.save()` on a receiver nobody could type. Not a write the count can
3252
+ * attribute — and not silence either: the reviewed application had six EIs
3253
+ * counted as EOs under a coverage of 99.5%, because a write on an unknown
3254
+ * local was dropped without a word. It lowers coverage and is named.
3255
+ */
3256
+ if (isUnreadableWrite(call, symbols, imports, packageImports, body) && !isNoise(call, owner)) {
3257
+ unresolved.push({
3258
+ file: ref.file,
3259
+ line: call.getStartLineNumber(),
3260
+ expression: call.getExpression().getText().replace(/\s+/g, ""),
3261
+ reason: "write on a receiver whose type the analysis cannot read"
3262
+ });
1713
3263
  continue;
1714
3264
  }
1715
3265
  if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner)) unresolved.push({
@@ -1723,12 +3273,21 @@ function createAnalyzer(app, stores, options = {}) {
1723
3273
  * Read after the loop: whether a key holds a nested transformer is known only
1724
3274
  * once the strategies have said which calls they follow.
1725
3275
  */
1726
- const output = outputFieldsIn(body, owner, storesByName, (c) => followedCalls.has(c));
3276
+ const followed = (c) => followedCalls.has(c);
3277
+ const output = outputFieldsIn(body, owner, storesByName, followed);
3278
+ const deliveries = deliveriesIn({
3279
+ body,
3280
+ file,
3281
+ symbols,
3282
+ relations: relationsByStore,
3283
+ followed: followedCalls
3284
+ });
1727
3285
  return {
1728
3286
  accesses,
1729
3287
  followUps,
1730
3288
  unresolved,
1731
3289
  validators: validator.fields,
3290
+ commandFields,
1732
3291
  opaqueValidators: validator.opaque,
1733
3292
  requestFields: request.fields,
1734
3293
  opaqueRequest: request.opaque,
@@ -1736,6 +3295,7 @@ function createAnalyzer(app, stores, options = {}) {
1736
3295
  opaqueOutputs: output.opaqueOutputs,
1737
3296
  transformed: output.resource,
1738
3297
  reads,
3298
+ deliveries,
1739
3299
  bodyHash: hashOf(body)
1740
3300
  };
1741
3301
  }
@@ -1781,7 +3341,7 @@ function createAnalyzer(app, stores, options = {}) {
1781
3341
  */
1782
3342
  if (!isApplicationCode(app.root, file.getFilePath())) {
1783
3343
  if (isSeeder(app.root, file.getFilePath())) {
1784
- const symbols = storeSymbolsFor(file, file, app, storesByName);
3344
+ const symbols = storeSymbolsFor(file, file, app, storesByName, relationsByStore);
1785
3345
  for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
1786
3346
  const access = symbols.size > 0 ? detectAccess(call, symbols, relationsByStore) : null;
1787
3347
  if (access?.mode !== "write") continue;
@@ -1791,7 +3351,7 @@ function createAnalyzer(app, stores, options = {}) {
1791
3351
  }
1792
3352
  continue;
1793
3353
  }
1794
- const symbols = storeSymbolsFor(file, file, app, storesByName);
3354
+ const symbols = storeSymbolsFor(file, file, app, storesByName, relationsByStore);
1795
3355
  if (symbols.size === 0) continue;
1796
3356
  for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
1797
3357
  const access = detectAccess(call, symbols, relationsByStore);
@@ -1835,18 +3395,156 @@ function createAnalyzer(app, stores, options = {}) {
1835
3395
  const touches = /* @__PURE__ */ new Set();
1836
3396
  const writtenStores = /* @__PURE__ */ new Set();
1837
3397
  const inputFields = /* @__PURE__ */ new Set();
3398
+ const commandFields = /* @__PURE__ */ new Set();
1838
3399
  const opaqueInputFields = /* @__PURE__ */ new Set();
1839
3400
  const requestFields = /* @__PURE__ */ new Set();
1840
3401
  let opaqueRequest = false;
1841
3402
  const outputFields = /* @__PURE__ */ new Set();
1842
3403
  const opaqueOutputFields = /* @__PURE__ */ new Set();
1843
3404
  const transformedStores = /* @__PURE__ */ new Set();
3405
+ const deliveredFields = /* @__PURE__ */ new Set();
3406
+ const deliveredOpaque = /* @__PURE__ */ new Set();
3407
+ const deliveredStores = /* @__PURE__ */ new Set();
3408
+ /** stores handed raw to a page: what the page shows of them is read after the walk */
3409
+ const rawDeliveries = [];
3410
+ let anyDelivery = false;
1844
3411
  const outputReads = /* @__PURE__ */ new Map();
1845
3412
  const trace = [];
1846
3413
  const scope = [];
1847
3414
  const unresolved = [];
1848
3415
  const visited = /* @__PURE__ */ new Set();
1849
3416
  let writes = false;
3417
+ /** what a followed body reads, itself and through what it follows — bounded like the walk */
3418
+ const storesReadBy = (ref, depth, seen = /* @__PURE__ */ new Set()) => {
3419
+ const found = /* @__PURE__ */ new Set();
3420
+ const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
3421
+ if (seen.has(key) || depth > maxDepth) return found;
3422
+ seen.add(key);
3423
+ const facts = factsFor(ref);
3424
+ if (!facts) return found;
3425
+ for (const access of facts.accesses) found.add(access.store);
3426
+ for (const followUp of facts.followUps) for (const store of storesReadBy(followUp.ref, depth + 1, seen)) found.add(store);
3427
+ return found;
3428
+ };
3429
+ /**
3430
+ * A delivered call hands on what its body RETURNS — classified, so a returned
3431
+ * `{ data: rows, meta }` delivers the rows' store and the meta's leaves, not
3432
+ * one DET per key. `pick` keeps one key of it: `const { data } = …`,
3433
+ * `resultado.linhas`. Bounded by depth like the walk, and by a seen set so a
3434
+ * body returning itself cannot loop.
3435
+ */
3436
+ const deliver = (item, depth, seen = /* @__PURE__ */ new Set()) => {
3437
+ switch (item.kind) {
3438
+ case "store":
3439
+ deliveredStores.add(item.store);
3440
+ if (item.via) rawDeliveries.push({
3441
+ ...item.via,
3442
+ store: item.store,
3443
+ path: item.path
3444
+ });
3445
+ return;
3446
+ case "scalar":
3447
+ deliveredFields.add(item.path || "<value>");
3448
+ return;
3449
+ case "echo": return;
3450
+ case "opaque":
3451
+ deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
3452
+ return;
3453
+ case "call": {
3454
+ if (depth > maxDepth + 2) return;
3455
+ let resolved = false;
3456
+ for (const ref of item.refs) {
3457
+ const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}#${item.pick ?? ""}#${item.path}`;
3458
+ if (seen.has(key)) continue;
3459
+ seen.add(key);
3460
+ const facts = factsFor(ref);
3461
+ if (!facts) continue;
3462
+ if (facts.outputs.length > 0 || facts.transformed) {
3463
+ resolved = true;
3464
+ continue;
3465
+ }
3466
+ const returned = facts.deliveries.returns.filter((r) => !item.pick || r.path === item.pick || r.path.startsWith(`${item.pick}.`));
3467
+ /**
3468
+ * `egresso.curso` where the body returns the row itself (`return
3469
+ * Egresso.query()…first()`, path ''): the pick lands INSIDE a returned
3470
+ * value — one field of a store is one value; one key of a returned call
3471
+ * is that call picked deeper.
3472
+ */
3473
+ if (item.pick && returned.length === 0) {
3474
+ const above = facts.deliveries.returns.filter((r) => r.path === "" || item.pick.startsWith(`${r.path}.`));
3475
+ for (const r of above) {
3476
+ const rest = r.path ? item.pick.slice(r.path.length + 1) : item.pick;
3477
+ if (r.kind === "call") deliver({
3478
+ ...r,
3479
+ path: item.path,
3480
+ pick: rest,
3481
+ via: item.via
3482
+ }, depth + 1, seen);
3483
+ else if (r.kind === "store" || r.kind === "scalar") deliveredFields.add(item.path || "<value>");
3484
+ }
3485
+ if (above.length > 0) {
3486
+ resolved = true;
3487
+ continue;
3488
+ }
3489
+ }
3490
+ /**
3491
+ * A return the classifier could not read at all (`rows.map(…).join(…)`)
3492
+ * says nothing about what leaves; what the body read, or what was handed
3493
+ * into it, says more — so it does not count as resolved.
3494
+ */
3495
+ if (returned.length > 0 && returned.some((r) => r.kind !== "opaque")) {
3496
+ for (const r of returned) {
3497
+ const rest = item.pick ? r.path.slice(item.pick.length).replace(/^\./, "") : r.path;
3498
+ const path = [item.path, rest].filter(Boolean).join(".");
3499
+ deliver({
3500
+ ...r,
3501
+ path,
3502
+ via: item.via
3503
+ }, depth + 1, seen);
3504
+ }
3505
+ resolved = true;
3506
+ continue;
3507
+ }
3508
+ if (item.pick) continue;
3509
+ const read = storesReadBy(ref, depth + 1);
3510
+ if (read.size > 0) {
3511
+ for (const store of read) {
3512
+ deliveredStores.add(store);
3513
+ if (item.via) rawDeliveries.push({
3514
+ ...item.via,
3515
+ store,
3516
+ path: item.path
3517
+ });
3518
+ }
3519
+ resolved = true;
3520
+ }
3521
+ }
3522
+ if (resolved) return;
3523
+ /**
3524
+ * `const { confidenciais } = await this.contar()` where the body's return
3525
+ * is unreadable: one KEY of it, named — one value, not an opaque floor.
3526
+ * Only a key that carries rows on (`data`, `rows`) stays unreadable.
3527
+ */
3528
+ if (item.pick && !PASSES_ROWS.has(item.pick.split(".")[0])) {
3529
+ deliveredFields.add(item.path || "<value>");
3530
+ return;
3531
+ }
3532
+ /**
3533
+ * The body returned no literal and read no store — a CSV builder, a
3534
+ * formatter over rows handed in. The document it built carries what it
3535
+ * received, so the rows' stores leave. Nothing handed in: opaque.
3536
+ */
3537
+ if (item.args.length > 0) {
3538
+ for (const argument of item.args) deliver({
3539
+ ...argument,
3540
+ via: item.via
3541
+ }, depth, seen);
3542
+ return;
3543
+ }
3544
+ deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
3545
+ }
3546
+ }
3547
+ };
1850
3548
  const visit = (ref, depth, technical = false) => {
1851
3549
  const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
1852
3550
  if (visited.has(key) || depth > maxDepth) return;
@@ -1885,12 +3583,20 @@ function createAnalyzer(app, stores, options = {}) {
1885
3583
  }
1886
3584
  unresolved.push(...facts.unresolved);
1887
3585
  for (const field of facts.validators) inputFields.add(field);
3586
+ for (const field of facts.commandFields) commandFields.add(field);
1888
3587
  for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
1889
3588
  for (const field of facts.requestFields) requestFields.add(field);
1890
3589
  if (facts.opaqueRequest) opaqueRequest = true;
1891
3590
  for (const field of facts.outputs) outputFields.add(field);
1892
3591
  for (const field of facts.opaqueOutputs) opaqueOutputFields.add(field);
1893
3592
  if (facts.transformed) transformedStores.add(facts.transformed);
3593
+ /**
3594
+ * Deliveries, resolved here because a delivered CALL leads to a body only
3595
+ * the graph knows: what that body returns is what the value hands on.
3596
+ */
3597
+ const items = [...facts.deliveries.calls, ...depth === 0 ? facts.deliveries.returns : []];
3598
+ if (facts.deliveries.anyCall || depth === 0 && facts.deliveries.anyReturn) anyDelivery = true;
3599
+ for (const item of items) deliver(item, depth);
1894
3600
  for (const { store, shape, columns, via } of facts.reads) {
1895
3601
  const known = outputReads.get(store) ?? {
1896
3602
  whole: false,
@@ -1926,17 +3632,38 @@ function createAnalyzer(app, stores, options = {}) {
1926
3632
  }
1927
3633
  };
1928
3634
  visit(handler, 0);
3635
+ /**
3636
+ * What each page shows of the stores handed to it raw — read once the walk is
3637
+ * done, because a query object's rows reach the page through the delivery of a
3638
+ * call, not of a variable (plan 0.8 §D).
3639
+ */
3640
+ const pages = readPages(rawDeliveries.filter((d) => !transformedStores.has(d.store)), {
3641
+ root: app.root,
3642
+ project,
3643
+ stores: storesByName,
3644
+ relations: relationsByStore,
3645
+ resolveSpecifier: app.resolveSpecifier
3646
+ });
1929
3647
  return {
1930
3648
  writes,
1931
3649
  touches: [...touches].sort(),
1932
3650
  writtenStores: [...writtenStores].sort(),
1933
3651
  inputFields: [...inputFields].sort(),
3652
+ commandFields: [...commandFields].sort(),
1934
3653
  opaqueInputFields: [...opaqueInputFields].sort(),
1935
3654
  requestFields: [...requestFields].sort(),
1936
3655
  opaqueRequest,
1937
3656
  outputFields: [...outputFields].sort(),
1938
3657
  opaqueOutputFields: [...opaqueOutputFields].sort(),
1939
3658
  transformedStores: [...transformedStores].sort(),
3659
+ delivered: {
3660
+ any: anyDelivery,
3661
+ fields: [...deliveredFields].sort(),
3662
+ opaqueFields: [...deliveredOpaque].sort(),
3663
+ stores: [...deliveredStores].sort()
3664
+ },
3665
+ pageReads: Object.fromEntries([...pages.columns.entries()].map(([store, columns]) => [store, [...columns].sort()])),
3666
+ unreadablePages: Object.fromEntries(pages.unreadable),
1940
3667
  outputReads: Object.fromEntries([...outputReads.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([store, read]) => [store, {
1941
3668
  whole: read.whole,
1942
3669
  selected: [...read.selected].sort(),
@@ -1969,6 +3696,8 @@ function findBody(file, ref) {
1969
3696
  }
1970
3697
  const fn = file.getFunction(ref.member);
1971
3698
  if (fn) return fn;
3699
+ const initializer = file.getVariableDeclaration(ref.member)?.getInitializer();
3700
+ if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) return initializer;
1972
3701
  return null;
1973
3702
  }
1974
3703
  for (const cls of file.getClasses()) {
@@ -1986,7 +3715,7 @@ function findBody(file, ref) {
1986
3715
  * from them: `const invite = await Invite.findOrFail(...)` makes `invite.save()`
1987
3716
  * count as a write to `Invite`.
1988
3717
  */
1989
- function storeSymbolsFor(body, file, app, stores) {
3718
+ function storeSymbolsFor(body, file, app, stores, relations = /* @__PURE__ */ new Map()) {
1990
3719
  const symbols = /* @__PURE__ */ new Map();
1991
3720
  for (const declaration of file.getImportDeclarations()) {
1992
3721
  if (!app.resolveSpecifier(declaration.getModuleSpecifierValue())) continue;
@@ -1997,10 +3726,48 @@ function storeSymbolsFor(body, file, app, stores) {
1997
3726
  if (stores.has(named.getName())) symbols.set(binding, named.getName());
1998
3727
  }
1999
3728
  }
3729
+ /**
3730
+ * `const { default: Noticia } = await import('#noticias/models/noticia')`: a
3731
+ * model imported INSIDE the body — an ace command does this to keep the app
3732
+ * from booting for `--help`. The store is the same; only the binding moved.
3733
+ */
3734
+ const storeByFile = /* @__PURE__ */ new Map();
3735
+ for (const [name, store] of stores) storeByFile.set(toPosix(store.provenance.file), name);
3736
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
3737
+ const binding = declaration.getNameNode();
3738
+ if (!Node.isObjectBindingPattern(binding)) continue;
3739
+ let initializer = declaration.getInitializer();
3740
+ if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
3741
+ if (!initializer || !Node.isCallExpression(initializer)) continue;
3742
+ if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
3743
+ const specifier = initializer.getArguments()[0];
3744
+ if (!specifier || !Node.isStringLiteral(specifier)) continue;
3745
+ const target = app.resolveSpecifier(specifier.getLiteralValue());
3746
+ const store = target ? storeByFile.get(toPosix(target)) : void 0;
3747
+ if (!store) continue;
3748
+ for (const element of binding.getElements()) if ((element.getPropertyNameNode()?.getText() ?? element.getName()) === "default") symbols.set(element.getName(), store);
3749
+ }
2000
3750
  for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
2001
3751
  const initializer = declaration.getInitializer();
2002
3752
  const name = declaration.getNameNode();
2003
- if (!initializer || !Node.isIdentifier(name)) continue;
3753
+ if (!Node.isIdentifier(name)) continue;
3754
+ const declared = storeNamedBy(declaration.getTypeNode()?.getText(), stores);
3755
+ if (declared) {
3756
+ symbols.set(name.getText(), declared);
3757
+ continue;
3758
+ }
3759
+ if (!initializer) continue;
3760
+ /**
3761
+ * `const pasta = documento.pasta`: a RELATION read off a row is the relation's
3762
+ * target, not the row's store — `pasta.save()` had been billed to `Documento`.
3763
+ * A plain field (`documento.nome`) is no store at all.
3764
+ */
3765
+ const plain = unwrapAwait(initializer);
3766
+ if (Node.isPropertyAccessExpression(plain)) {
3767
+ const target = storeOfExpression(plain, symbols, relations);
3768
+ if (target) symbols.set(name.getText(), target);
3769
+ continue;
3770
+ }
2004
3771
  const root = rootSymbolOf(initializer);
2005
3772
  const store = root ? symbols.get(root) : void 0;
2006
3773
  if (store) symbols.set(name.getText(), store);
@@ -2008,8 +3775,8 @@ function storeSymbolsFor(body, file, app, stores) {
2008
3775
  if (Node.isMethodDeclaration(body) || Node.isFunctionDeclaration(body)) for (const parameter of body.getParameters()) {
2009
3776
  const typeNode = parameter.getTypeNode();
2010
3777
  const nameNode = parameter.getNameNode();
2011
- const typeName = typeNode?.getText();
2012
- if (typeName && stores.has(typeName) && Node.isIdentifier(nameNode)) {
3778
+ const typeName = storeNamedBy(typeNode?.getText(), stores);
3779
+ if (typeName && Node.isIdentifier(nameNode)) {
2013
3780
  symbols.set(nameNode.getText(), typeName);
2014
3781
  continue;
2015
3782
  }
@@ -2020,32 +3787,56 @@ function storeSymbolsFor(body, file, app, stores) {
2020
3787
  * Registers the PATH `input.invite`, because that is how the write
2021
3788
  * appears: `input.invite.save()`.
2022
3789
  */
2023
- if (typeNode && Node.isIdentifier(nameNode)) {
2024
- for (const [property, propertyType] of membersOfType(typeNode, file, app)) if (stores.has(propertyType)) symbols.set(`${nameNode.getText()}.${property}`, propertyType);
3790
+ if (typeNode && Node.isIdentifier(nameNode)) for (const [property, propertyType] of membersOfType(typeNode, file, app)) {
3791
+ const named = storeNamedBy(propertyType, stores);
3792
+ if (named) symbols.set(`${nameNode.getText()}.${property}`, named);
2025
3793
  }
2026
3794
  /**
2027
- * Destructured form: `handle({ invite }: { invite: Invite })`.
2028
- *
2029
- * This is the dominant shape in the action-object pattern — the action
2030
- * receives a named payload. Without it, `invite.save()` inside the action
2031
- * does not count as a write, and the whole transaction becomes an EO
2032
- * instead of an EI.
3795
+ * Destructured form: `handle({ invite }: { invite: Invite })`, and
3796
+ * `handle({ document, name }: RenameDocumentInput)` with the interface named
3797
+ * — in this file or imported. The second is the dominant shape on a reviewed
3798
+ * application, and only the inline literal was read: every `document.save()`
3799
+ * behind it was invisible, and the transaction an EO (plan 0.8 §A).
2033
3800
  */
2034
3801
  const binding = nameNode.asKind(SyntaxKind.ObjectBindingPattern);
2035
- const literal = typeNode?.asKind(SyntaxKind.TypeLiteral);
2036
- if (!binding || !literal) continue;
2037
- const propertyTypes = /* @__PURE__ */ new Map();
2038
- for (const member of literal.getMembers()) {
2039
- if (!Node.isPropertySignature(member)) continue;
2040
- const memberType = member.getTypeNode()?.getText();
2041
- if (memberType) propertyTypes.set(member.getName(), memberType);
3802
+ if (!binding || !typeNode) continue;
3803
+ const propertyTypes = membersOfType(typeNode, file, app);
3804
+ for (const element of binding.getElements()) {
3805
+ const property = element.getPropertyNameNode()?.getText() ?? element.getName();
3806
+ const resolved = storeNamedBy(propertyTypes.get(property), stores);
3807
+ if (resolved) symbols.set(element.getName(), resolved);
2042
3808
  }
3809
+ }
3810
+ /**
3811
+ * `const { preIntake } = input` where `input.preIntake` is a registered path:
3812
+ * each element inherits the store of its path. Read AFTER the parameters, which
3813
+ * is where the paths come from.
3814
+ */
3815
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
3816
+ const binding = declaration.getNameNode();
3817
+ const initializer = declaration.getInitializer();
3818
+ if (!Node.isObjectBindingPattern(binding) || !initializer) continue;
3819
+ const root = Node.isIdentifier(initializer) ? initializer.getText() : null;
3820
+ if (!root) continue;
2043
3821
  for (const element of binding.getElements()) {
2044
3822
  const property = element.getPropertyNameNode()?.getText() ?? element.getName();
2045
- const resolved = propertyTypes.get(property);
2046
- if (resolved && stores.has(resolved)) symbols.set(element.getName(), resolved);
3823
+ const store = symbols.get(`${root}.${property}`);
3824
+ if (store) symbols.set(element.getName(), store);
2047
3825
  }
2048
3826
  }
3827
+ /**
3828
+ * `for (const documento of pasta.documentos)`, `for (const row of rows)`: the
3829
+ * loop variable is a row of the relation's target, or of the rows' store. A
3830
+ * `delete()` inside such a loop was invisible; read last, after the parameters that name the parent — plan 0.8 §A.
3831
+ */
3832
+ for (const loop of body.getDescendantsOfKind(SyntaxKind.ForOfStatement)) {
3833
+ const declared = loop.getInitializer();
3834
+ if (!Node.isVariableDeclarationList(declared)) continue;
3835
+ const nameNode = declared.getDeclarations()[0]?.getNameNode();
3836
+ if (!nameNode || !Node.isIdentifier(nameNode)) continue;
3837
+ const store = storeOfExpression(unwrapAwait(loop.getExpression()), symbols, relations);
3838
+ if (store) symbols.set(nameNode.getText(), store);
3839
+ }
2049
3840
  return symbols;
2050
3841
  }
2051
3842
  /**
@@ -2109,6 +3900,67 @@ function resolveTypeToFile(typeName, file, app) {
2109
3900
  * Accepts an inline type literal and a named type declared in this file or
2110
3901
  * imported from the application. Anything else yields empty — no guessing.
2111
3902
  */
3903
+ /**
3904
+ * The store a type annotation names, or nothing: `Sessao`, `Sessao | null`,
3905
+ * `Promise<Sessao | null>`, `Sessao[]`, `Promise<Sessao[]>`. Two different stores
3906
+ * in one union name nothing — a guess is not a binding.
3907
+ */
3908
+ function storeNamedBy(typeText, stores) {
3909
+ if (!typeText) return null;
3910
+ let text = typeText.trim();
3911
+ const promise = text.match(/^Promise<([\s\S]*)>$/);
3912
+ if (promise) text = promise[1];
3913
+ const named = new Set(text.split("|").map((part) => part.trim().replace(/\[\]$/, "").replace(/^Array<(.*)>$/, "$1").trim()).filter((part) => part && part !== "null" && part !== "undefined"));
3914
+ if (named.size !== 1) return null;
3915
+ const [only] = named;
3916
+ return stores.has(only) ? only : null;
3917
+ }
3918
+ /** array methods that hand back one row, or the same rows, of the receiver */
3919
+ const ONE_OF_ROWS = new Set([
3920
+ "find",
3921
+ "findLast",
3922
+ "at",
3923
+ "filter",
3924
+ "slice",
3925
+ "sort",
3926
+ "toSorted",
3927
+ "reverse",
3928
+ "toReversed",
3929
+ "concat",
3930
+ "flat",
3931
+ "first",
3932
+ "last"
3933
+ ]);
3934
+ /** array methods whose callback receives one row of the receiver */
3935
+ const ITERATES_ROWS = new Set([
3936
+ "map",
3937
+ "forEach",
3938
+ "filter",
3939
+ "find",
3940
+ "findLast",
3941
+ "some",
3942
+ "every",
3943
+ "flatMap",
3944
+ "reduce"
3945
+ ]);
3946
+ const lastSegmentText = (node) => Node.isPropertyAccessExpression(node) ? node.getName() : node.getText();
3947
+ /** the store an expression is rows of: a store symbol, or `parent.relation` with the relation declared */
3948
+ function storeOfExpression(expression, symbols, relations) {
3949
+ if (!expression) return null;
3950
+ if (Node.isIdentifier(expression)) return symbols.get(expression.getText()) ?? null;
3951
+ if (Node.isPropertyAccessExpression(expression)) {
3952
+ const receiver = expression.getExpression();
3953
+ if (Node.isIdentifier(receiver)) {
3954
+ const byPath = symbols.get(`${receiver.getText()}.${expression.getName()}`);
3955
+ if (byPath) return byPath;
3956
+ }
3957
+ const parent = symbols.get(rootSymbolOf(receiver) ?? "");
3958
+ if (!parent) return null;
3959
+ return relations.get(parent)?.[expression.getName()] ?? null;
3960
+ }
3961
+ const root = rootSymbolOf(expression);
3962
+ return root ? symbols.get(root) ?? null : null;
3963
+ }
2112
3964
  function membersOfType(typeNode, file, app) {
2113
3965
  const members = /* @__PURE__ */ new Map();
2114
3966
  const collect = (node) => {
@@ -2162,6 +4014,32 @@ function importMapsOf(file, app) {
2162
4014
  if (alias) exportedAs.set(alias, named.getName());
2163
4015
  }
2164
4016
  }
4017
+ /**
4018
+ * `const { default: SincronizarBulk } = await import('#inpi/actions/sincronizar_bulk')`
4019
+ * `const { execucaoEmAndamento } = await import('#inpi/services/execucao')`
4020
+ *
4021
+ * A module imported INSIDE a body — the shape ace commands use so `--help` does
4022
+ * not boot the application. The binding moved; the body it names did not, and
4023
+ * a command importing its whole action layer this way reached nothing.
4024
+ */
4025
+ for (const declaration of file.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
4026
+ const binding = declaration.getNameNode();
4027
+ if (!Node.isObjectBindingPattern(binding)) continue;
4028
+ let initializer = declaration.getInitializer();
4029
+ if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
4030
+ if (!initializer || !Node.isCallExpression(initializer)) continue;
4031
+ if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
4032
+ const specifier = initializer.getArguments()[0];
4033
+ if (!specifier || !Node.isStringLiteral(specifier)) continue;
4034
+ const target = app.resolveSpecifier(specifier.getLiteralValue());
4035
+ if (!target) continue;
4036
+ for (const element of binding.getElements()) {
4037
+ const exported = element.getPropertyNameNode()?.getText() ?? element.getName();
4038
+ const local = element.getName();
4039
+ imports.set(local, target);
4040
+ if (exported !== "default" && exported !== local) exportedAs.set(local, exported);
4041
+ }
4042
+ }
2165
4043
  return {
2166
4044
  imports,
2167
4045
  exportedAs
@@ -2197,6 +4075,77 @@ function isWorthReporting(call, symbols, imports) {
2197
4075
  if (root === "this") return true;
2198
4076
  return imports.has(root);
2199
4077
  }
4078
+ /** Lucid's persistence on an instance, with no arguments: `x.save()`, `x.delete()` — a `Map#delete(key)` has one */
4079
+ const INSTANCE_WRITES = new Set([
4080
+ "save",
4081
+ "delete",
4082
+ "forceDelete"
4083
+ ]);
4084
+ /** persistence through a relation or a merge: `x.related('y').create(…)`, `x.merge(p).save()` */
4085
+ const CHAINED_WRITES = new Set([
4086
+ "create",
4087
+ "createMany",
4088
+ "save",
4089
+ "saveMany",
4090
+ "attach",
4091
+ "detach",
4092
+ "sync",
4093
+ "updateOrCreate",
4094
+ "firstOrCreate",
4095
+ "delete"
4096
+ ]);
4097
+ const WRITE_CHAINS = new Set([
4098
+ "related",
4099
+ "merge",
4100
+ "fill",
4101
+ "useTransaction"
4102
+ ]);
4103
+ /**
4104
+ * A persistence call on a receiver the body cannot type: a local that is neither a
4105
+ * store, nor an import, nor `this`, nor the result of a call on one of those.
4106
+ * Reported as unresolved — the count must not stay silent where an EI may hide.
4107
+ */
4108
+ function isUnreadableWrite(call, symbols, imports, packageImports, body) {
4109
+ const expression = call.getExpression();
4110
+ if (!Node.isPropertyAccessExpression(expression)) return false;
4111
+ const method = expression.getName();
4112
+ const receiver = unwrapAwait(expression.getExpression());
4113
+ if (Node.isCallExpression(receiver)) {
4114
+ const inner = receiver.getExpression();
4115
+ if (!Node.isPropertyAccessExpression(inner) || !WRITE_CHAINS.has(inner.getName())) return false;
4116
+ if (!CHAINED_WRITES.has(method)) return false;
4117
+ } else if (!INSTANCE_WRITES.has(method) || call.getArguments().length > 0) return false;
4118
+ const root = rootSymbolOf(expression.getExpression());
4119
+ if (!root || root === "this") return false;
4120
+ if (symbols.has(root) || imports.has(root) || packageImports.has(root)) return false;
4121
+ if (!Node.isIdentifier(unwrapAwait(rootNodeOf(expression.getExpression())))) return false;
4122
+ /**
4123
+ * `const pdfDoc = await PDFDocument.create()` from `pdf-lib`, then `pdfDoc.save()`:
4124
+ * a package's object with a method called `save`. Its declaration says where it
4125
+ * came from, and it is not a store — nothing to report.
4126
+ */
4127
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
4128
+ const nameNode = declaration.getNameNode();
4129
+ if (!Node.isIdentifier(nameNode) || nameNode.getText() !== root) continue;
4130
+ const initializer = declaration.getInitializer();
4131
+ const origin = initializer ? rootSymbolOf(unwrapAwait(initializer)) : null;
4132
+ if (origin && packageImports.has(origin)) return false;
4133
+ }
4134
+ return true;
4135
+ }
4136
+ /** the leftmost node of a chain */
4137
+ function rootNodeOf(node) {
4138
+ let current = node;
4139
+ for (let depth = 0; depth < 40; depth++) {
4140
+ const next = unwrapAwait(current);
4141
+ if (Node.isPropertyAccessExpression(next) || Node.isCallExpression(next) || Node.isElementAccessExpression(next)) {
4142
+ current = next.getExpression();
4143
+ continue;
4144
+ }
4145
+ return next;
4146
+ }
4147
+ return current;
4148
+ }
2200
4149
  /** `(await x())` and `x()` are the same receiver for this purpose. */
2201
4150
  function unwrapAwait(node) {
2202
4151
  let current = node;
@@ -2215,6 +4164,154 @@ function hashOf(body) {
2215
4164
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
2216
4165
  }
2217
4166
  //#endregion
4167
+ //#region src/inventory/sources/jobs.ts
4168
+ /** `X.schedule({}).cron(…)`: a scheduler's way of dispatching */
4169
+ const SCHEDULE_METHODS = new Set(["schedule"]);
4170
+ function collectJobs(app) {
4171
+ const project = new Project({
4172
+ skipAddingFilesFromTsConfig: true,
4173
+ skipFileDependencyResolution: true,
4174
+ compilerOptions: { allowJs: false }
4175
+ });
4176
+ const root = toPosix(app.root);
4177
+ for (const dir of app.scanRoots) project.addSourceFilesAtPaths(`${toPosix(dir)}/**/*.ts`);
4178
+ project.addSourceFilesAtPaths(`${root}/start/**/*.ts`);
4179
+ project.addSourceFilesAtPaths(`${root}/commands/**/*.ts`);
4180
+ const jobs = /* @__PURE__ */ new Map();
4181
+ for (const file of project.getSourceFiles()) {
4182
+ const filePath = toPosix(file.getFilePath());
4183
+ if (!isApplicationCode(app.root, filePath) || !inJobsDirectory(filePath)) continue;
4184
+ for (const cls of file.getClasses()) {
4185
+ const name = cls.getName();
4186
+ if (!name || !looksLikeJob(cls)) continue;
4187
+ jobs.set(filePath, {
4188
+ name,
4189
+ file: filePath,
4190
+ dispatchedFrom: [],
4191
+ scheduledFrom: []
4192
+ });
4193
+ }
4194
+ }
4195
+ if (jobs.size === 0) return [];
4196
+ for (const file of project.getSourceFiles()) {
4197
+ const filePath = toPosix(file.getFilePath());
4198
+ if (/\.(spec|test)\.ts$/.test(file.getBaseName())) continue;
4199
+ const { imports } = importMapsOf(file, app);
4200
+ if (imports.size === 0) continue;
4201
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
4202
+ const callee = call.getExpression();
4203
+ if (!Node.isPropertyAccessExpression(callee)) continue;
4204
+ const method = callee.getName();
4205
+ const scheduled = SCHEDULE_METHODS.has(method);
4206
+ if (!scheduled && !DISPATCH_METHODS.has(method)) continue;
4207
+ const receiver = callee.getExpression();
4208
+ if (!Node.isIdentifier(receiver)) continue;
4209
+ const target = imports.get(receiver.getText());
4210
+ const job = target ? jobs.get(toPosix(target)) : void 0;
4211
+ if (!job) continue;
4212
+ const sites = scheduled ? job.scheduledFrom : job.dispatchedFrom;
4213
+ if (!sites.includes(filePath)) sites.push(filePath);
4214
+ }
4215
+ }
4216
+ return [...jobs.values()].sort((a, b) => a.name.localeCompare(b.name));
4217
+ }
4218
+ const inJobsDirectory = (filePath) => filePath.split("/").slice(0, -1).some((segment) => segment === "jobs" || segment === "job");
4219
+ /** extends something called `…Job`, or declares the method a queue would run */
4220
+ function looksLikeJob(cls) {
4221
+ const parent = cls.getExtends()?.getExpression().getText() ?? "";
4222
+ if (/Job$/.test(parent)) return true;
4223
+ return EXECUTION_METHODS.some((method) => cls.getMethod(method) !== void 0);
4224
+ }
4225
+ //#endregion
4226
+ //#region src/inventory/sources/json_schemas.ts
4227
+ function collectJsonSchemas(app) {
4228
+ const project = new Project({
4229
+ skipAddingFilesFromTsConfig: true,
4230
+ skipFileDependencyResolution: true,
4231
+ compilerOptions: { allowJs: false }
4232
+ });
4233
+ /**
4234
+ * Wider than `scanRoots`, and only here.
4235
+ *
4236
+ * `database/` is excluded from the application roots on purpose: test factories
4237
+ * and migrations contain real persistence calls, and scanning them would turn a
4238
+ * test write into a counted function. But `node ace make:seeder` puts seeders in
4239
+ * `database/seeders`, which is where seed data — and therefore a form's schema —
4240
+ * normally lives. Reading a literal counts nothing, so there is no conflict: the
4241
+ * exclusion protects the call graph, not the schema catalogue.
4242
+ *
4243
+ * Without this, `overrides.detFromSchema` naming a schema declared in a seeder
4244
+ * reported "not declared anywhere in the code" and left the DET count at the
4245
+ * floor — the exact case the override exists for.
4246
+ */
4247
+ for (const root of [...app.scanRoots.map(toPosix), `${toPosix(app.root)}/database`]) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
4248
+ const found = /* @__PURE__ */ new Map();
4249
+ for (const file of project.getSourceFiles()) for (const declaration of file.getVariableDeclarations()) {
4250
+ const literal = unwrap(declaration.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
4251
+ if (!literal || !isJsonSchema(literal)) continue;
4252
+ const leaves = leavesOf(literal);
4253
+ if (leaves.length === 0) continue;
4254
+ found.set(declaration.getName(), {
4255
+ name: declaration.getName(),
4256
+ fields: leaves.length,
4257
+ leaves,
4258
+ provenance: {
4259
+ file: toPosix(file.getFilePath()),
4260
+ line: declaration.getStartLineNumber(),
4261
+ by: "json-schema"
4262
+ }
4263
+ });
4264
+ }
4265
+ return found;
4266
+ }
4267
+ /** `{ … } as const` and `{ … } satisfies X` still hold the literal */
4268
+ function unwrap(node) {
4269
+ if (!node) return void 0;
4270
+ if (Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) return unwrap(node.getExpression());
4271
+ return node;
4272
+ }
4273
+ /**
4274
+ * Recognised by shape, never by name.
4275
+ *
4276
+ * A const called `schema` may be anything; an object declaring `type: 'object'`
4277
+ * with a `properties` map is a JSON Schema whatever it is called.
4278
+ */
4279
+ function isJsonSchema(literal) {
4280
+ return (literal.getProperty("type")?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer()?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue() === "object" && literal.getProperty("properties") !== void 0;
4281
+ }
4282
+ /**
4283
+ * Leaves of a JSON Schema, by the table in counting-decisions §7.
4284
+ *
4285
+ * scalar 1
4286
+ * nested object leaves counted individually
4287
+ * array of scalar 1 (repeating group)
4288
+ * array of object the object's leaves, once
4289
+ * enum / const 1
4290
+ */
4291
+ function leavesOf(literal, prefix = "") {
4292
+ const properties = literal.getProperty("properties")?.asKind(SyntaxKind.PropertyAssignment);
4293
+ const items = literal.getProperty("items")?.asKind(SyntaxKind.PropertyAssignment);
4294
+ if (properties) {
4295
+ const map = properties.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
4296
+ if (!map) return [];
4297
+ return map.getProperties().flatMap((property) => {
4298
+ const assignment = property.asKind(SyntaxKind.PropertyAssignment);
4299
+ if (!assignment) return [];
4300
+ const name = assignment.getName().replace(/['"]/g, "");
4301
+ const path = prefix ? `${prefix}.${name}` : name;
4302
+ const nested = unwrap(assignment.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
4303
+ const inner = nested ? leavesOf(nested, path) : [];
4304
+ return inner.length > 0 ? inner : [path];
4305
+ });
4306
+ }
4307
+ if (items) {
4308
+ const element = unwrap(items.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
4309
+ const inner = element ? leavesOf(element, prefix) : [];
4310
+ return inner.length > 0 ? inner : [prefix];
4311
+ }
4312
+ return [];
4313
+ }
4314
+ //#endregion
2218
4315
  //#region src/albrecht/tables.ts
2219
4316
  /** shared grid: [ref band][DET band] -> complexity */
2220
4317
  const GRID = [
@@ -2757,7 +4854,7 @@ function detsFor(entry, behavior, touched, type, options) {
2757
4854
  counted.add(field);
2758
4855
  sources.push(source);
2759
4856
  };
2760
- for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
4857
+ if (entry.kind === "http") for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
2761
4858
  /**
2762
4859
  * `(opaque)` is not decoration: `detFromSchema` replaces the opaque placeholder
2763
4860
  * with a schema's fields, and it used to assume there was exactly one and that
@@ -2777,6 +4874,11 @@ function detsFor(entry, behavior, touched, type, options) {
2777
4874
  * paid for twice.
2778
4875
  */
2779
4876
  for (const field of behavior.requestFields) add(field, `request:${field}`);
4877
+ for (const field of behavior.commandFields ?? []) {
4878
+ const [kind, ...rest] = field.split(".");
4879
+ const name = rest.join(".");
4880
+ add(name, `${kind === "args" ? "arg" : "flag"}:${name}`);
4881
+ }
2780
4882
  if (type === "EO" || type === "EQ") {
2781
4883
  /**
2782
4884
  * counting-decisions §6, per store, in order of what is visible:
@@ -2812,8 +4914,34 @@ function detsFor(entry, behavior, touched, type, options) {
2812
4914
  }
2813
4915
  }
2814
4916
  for (const field of behavior.outputFields) add(field, `transformer:${field}${opaqueOutputs.has(field) ? " (opaque)" : ""}`);
4917
+ /**
4918
+ * Plan 0.7 §A′ — the delivery is the boundary. When the transaction hands
4919
+ * something to a renderer or a response, THAT is what leaves: derived values
4920
+ * and the leaves of literals a followed body returned count 1 each; a store
4921
+ * leaves only if its rows were handed on, raw or through a query object; a
4922
+ * store read to authorise or to decide contributes nothing to the output. A
4923
+ * field that entered and is echoed back counts once, on entry (§7.3) — the
4924
+ * classifier marks it as an echo. Without a delivery point the rule cannot
4925
+ * apply, and every store read leaves, as before.
4926
+ */
4927
+ const delivered = behavior.delivered;
4928
+ if (delivered.any) {
4929
+ for (const field of delivered.fields) add(`render.${field}`, `render:${field}`);
4930
+ for (const field of delivered.opaqueFields) add(`render.${field}`, `render:${field} (opaque)`);
4931
+ }
4932
+ const deliveredStores = new Set(delivered.stores);
4933
+ for (let changed = true; changed;) {
4934
+ changed = false;
4935
+ for (const [store, read] of Object.entries(behavior.outputReads)) {
4936
+ if (deliveredStores.has(store) || read.direct) continue;
4937
+ if (!read.via.some((parent) => deliveredStores.has(parent))) continue;
4938
+ deliveredStores.add(store);
4939
+ changed = true;
4940
+ }
4941
+ }
2815
4942
  for (const store of touched) {
2816
4943
  const read = behavior.outputReads[store];
4944
+ if (delivered.any && !deliveredStores.has(store) && !covered.has(store)) continue;
2817
4945
  if (read?.aggregate) add(`${store}.<aggregate>`, `aggregate:${store} (a count or an existence check: one scalar)`);
2818
4946
  if (covered.has(store)) continue;
2819
4947
  if (read && read.aggregate && !read.whole && read.selected.length === 0) continue;
@@ -2839,6 +4967,19 @@ function detsFor(entry, behavior, touched, type, options) {
2839
4967
  }
2840
4968
  continue;
2841
4969
  }
4970
+ /**
4971
+ * Handed raw to a page the reader could open: the columns the page reads off
4972
+ * the rows are what the user sees (§6, plan 0.8 §D). A page it could not read
4973
+ * leaves the store whole below, and the count says why.
4974
+ */
4975
+ const shown = behavior.pageReads?.[store];
4976
+ if (shown && !behavior.unreadablePages?.[store]) {
4977
+ for (const column of shown) {
4978
+ if (excluded.has(column)) continue;
4979
+ add(`${store}.${column}`, `page:${store}.${column}${mark(column)}`);
4980
+ }
4981
+ continue;
4982
+ }
2842
4983
  for (const column of attributes) {
2843
4984
  if (excluded.has(column.name)) continue;
2844
4985
  add(`${store}.${column.name}`, `output:${store}.${column.name}${mark(column.name)}`);
@@ -2926,12 +5067,20 @@ const RULESET = "afp";
2926
5067
  * column; system timestamps and `serializeAs: null` columns leaving the DETs;
2927
5068
  * master-detail folded into one data function; identity by table; token tables
2928
5069
  * technical; and opaque declarations reaching every function carrying the origin.
5070
+ * Three in 1.6.0: an output's DETs are what the transaction DELIVERS (the render
5071
+ * props, the response payload, what a command prints) read back to their origin;
5072
+ * a function of the same file and a `.map(fn)` by reference are followed, so
5073
+ * FTRs move; and ace commands are transactions, with flags as input. One in
5074
+ * 1.7.0, wide: a write binds to what the variable IS — a destructured named
5075
+ * interface, a followed method's return, a relation off a row, a loop over rows,
5076
+ * the guard's user, a service the container made — and a write nobody can type is
5077
+ * an unresolved call instead of silence.
2929
5078
  *
2930
5079
  * Without the bump, a baseline saved by the previous version compares cleanly
2931
5080
  * against this one and bills the tool's own improvement as work done. The guard
2932
5081
  * exists for exactly that, and only this constant arms it.
2933
5082
  */
2934
- const RULESET_VERSION = "1.5.0";
5083
+ const RULESET_VERSION = "1.7.0";
2935
5084
  function count(input, options = {}) {
2936
5085
  const warnings = [];
2937
5086
  const usage = usageOf(input);
@@ -3013,7 +5162,8 @@ function count(input, options = {}) {
3013
5162
  const countedRoots = new Set(dataFunctions.map((fn) => fn.name));
3014
5163
  const countedStores = new Map(countable.filter((store) => countedRoots.has(grouping.rootOf.get(store.name) ?? store.name)).map((store) => [store.name, store]));
3015
5164
  const ignored = new Set(options.boundary?.ignoreEntryPoints ?? []);
3016
- const transactionalFunctions = countTransactionalFunctions(input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? "")), input.behaviors, {
5165
+ const entryPoints = input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? ""));
5166
+ const transactionalFunctions = countTransactionalFunctions(entryPoints, input.behaviors, {
3017
5167
  countedStores,
3018
5168
  grouping,
3019
5169
  root: input.app.root,
@@ -3047,6 +5197,10 @@ function count(input, options = {}) {
3047
5197
  }));
3048
5198
  warnings.push(...unreadableInputWarnings(input));
3049
5199
  warnings.push(...unreadableOutputWarnings(input));
5200
+ warnings.push(...unreadableDeliveryWarnings(input));
5201
+ warnings.push(...commandWarnings(entryPoints, transactionalFunctions));
5202
+ warnings.push(...undispatchedJobWarnings(input));
5203
+ warnings.push(...unreadablePageWarnings(input));
3050
5204
  warnings.push(...lookAlikeWarnings(functions));
3051
5205
  warnings.push(...seededOnlyWarnings(functions, grouping.members, input.seededAnywhere));
3052
5206
  return {
@@ -3058,6 +5212,88 @@ function count(input, options = {}) {
3058
5212
  };
3059
5213
  }
3060
5214
  /**
5215
+ * Transactions that deliver something the analysis cannot read.
5216
+ *
5217
+ * A generated document (`response.send(gerarCsv(rows))`), a value built by a
5218
+ * call nobody followed. Each counts 1 DET — a floor — and the transaction's
5219
+ * output is understated by whatever the value carries. Named with the
5220
+ * expression, because the fix is usually in the code: return a literal, or a
5221
+ * transformer, and the fields become readable.
5222
+ */
5223
+ function unreadableDeliveryWarnings(input) {
5224
+ const blind = input.entryPoints.map((entry) => ({
5225
+ entry,
5226
+ behavior: input.behaviors.get(entry.id)
5227
+ })).filter(({ behavior }) => behavior && !behavior.writes && behavior.delivered.opaqueFields.length > 0);
5228
+ if (blind.length === 0) return [];
5229
+ return [
5230
+ `${blind.length} transaction(s) deliver a value the analysis cannot read — a generated document, a call nobody followed — counted as 1 DET each, a FLOOR. This UNDERSTATES the output:`,
5231
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.delivered.opaqueFields.join(", ")}`),
5232
+ ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5233
+ ];
5234
+ }
5235
+ /**
5236
+ * Pages the reader could not open for a store handed to them raw — plan 0.8 §D.
5237
+ *
5238
+ * The store leaves whole, as it always did; this says which page, and why: a
5239
+ * second level of components, a spread, a package's component, two files answering
5240
+ * to one name. Overestimating in the open — the fix is usually in the page.
5241
+ */
5242
+ function unreadablePageWarnings(input) {
5243
+ const lines = [];
5244
+ for (const entry of input.entryPoints) {
5245
+ const behavior = input.behaviors.get(entry.id);
5246
+ if (!behavior || behavior.writes) continue;
5247
+ for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.trigger} ${entry.signature}: ${store} leaves whole — ${reason}`);
5248
+ }
5249
+ if (lines.length === 0) return [];
5250
+ return [
5251
+ `${lines.length} store(s) handed raw to a page the analysis could not read: every column counted. What the page shows would be less:`,
5252
+ ...lines.slice(0, 12),
5253
+ ...lines.length > 12 ? [` … and ${lines.length - 12} more`] : []
5254
+ ];
5255
+ }
5256
+ /**
5257
+ * Jobs no transaction reaches — plan 0.7 §D.
5258
+ *
5259
+ * A job dispatched by a handler is part of that handler's transaction (§9). One
5260
+ * that nothing reachable dispatches is either a scheduled process — an elementary
5261
+ * process nobody is counting — or dead code, and the code cannot say which. It is
5262
+ * reported, never counted: inventing an elementary process is the error this
5263
+ * package exists to avoid.
5264
+ */
5265
+ function undispatchedJobWarnings(input) {
5266
+ if (!input.jobs?.length) return [];
5267
+ const reached = /* @__PURE__ */ new Set();
5268
+ for (const behavior of input.behaviors.values()) for (const step of behavior.trace) reached.add(toPosix(step.file));
5269
+ const orphans = input.jobs.filter((job) => !reached.has(job.file));
5270
+ if (orphans.length === 0) return [];
5271
+ const where = (file) => relativeTo(input.app.root, file);
5272
+ return [`${orphans.length} job(s) reached by no transaction — a scheduled process nobody counts, or dead code. Reported, not counted: a scheduler is an entry point of its own once one is read (counting-decisions §9):`, ...orphans.map(({ name, file, dispatchedFrom, scheduledFrom }) => {
5273
+ const at = ` ${name} (${where(file)}): `;
5274
+ if (scheduledFrom.length > 0) return `${at}scheduled from ${scheduledFrom.map(where).join(", ")}, outside every transaction`;
5275
+ if (dispatchedFrom.length > 0) return `${at}dispatched from ${dispatchedFrom.map(where).join(", ")}, which no transaction reaches`;
5276
+ return `${at}dispatched by nothing in the application — dead code, or a scheduler this analysis does not read`;
5277
+ })];
5278
+ }
5279
+ /**
5280
+ * Ace commands counted as transactions — plan 0.7 §C.
5281
+ *
5282
+ * A batch process an operator starts is an elementary process, and it is counted.
5283
+ * Half the commands on the validated applications are development tools — a data
5284
+ * generator, a scaffolder — which the CPM does not count and the code cannot tell
5285
+ * from an importer: both write the same table. So each one is listed with the FP
5286
+ * at stake, the collector's hint beside it, and the way out is a declaration.
5287
+ */
5288
+ function commandWarnings(entryPoints, functions) {
5289
+ const counted = entryPoints.filter((entry) => entry.kind === "command").map((entry) => ({
5290
+ entry,
5291
+ fn: functions.find((f) => f.id === `tx:${entry.identity}`)
5292
+ })).filter(({ fn }) => fn !== void 0);
5293
+ if (counted.length === 0) return [];
5294
+ return [`${counted.length} ace command(s) counted as elementary processes — a batch process an operator starts is a transaction. A development tool (a data generator, a scaffolder) is not the user's: exclude it with \`boundary.ignoreEntryPoints: ['<commandName>']\`:`, ...counted.map(({ entry, fn }) => ` ${entry.identity}: ${fn.type} ${fn.points} FP` + (entry.hints?.length ? ` — ${entry.hints.join("; ")}` : ""))];
5295
+ }
5296
+ /**
3061
5297
  * Transactions that look like the same elementary process.
3062
5298
  *
3063
5299
  * The CPM counts identical processing logic once. `GET /perfil` and
@@ -3379,7 +5615,9 @@ var CoverageTooLowError = class extends Error {
3379
5615
  async function analyze(root, options = {}) {
3380
5616
  const app = await discoverApp(root);
3381
5617
  const { stores, unresolved: storeProblems } = await collectDataStores(app);
3382
- const { entryPoints, unresolved: routeProblems } = await collectEntryPoints(app);
5618
+ const routes = await collectEntryPoints(app);
5619
+ const routeProblems = routes.unresolved;
5620
+ const entryPoints = [...routes.entryPoints, ...collectCommands(app)];
3383
5621
  /** only read when an override names one — but collected once, like everything else */
3384
5622
  const jsonSchemas = collectJsonSchemas(app);
3385
5623
  const analyzer = createAnalyzer(app, stores, {
@@ -3461,6 +5699,13 @@ async function analyze(root, options = {}) {
3461
5699
  by: "request"
3462
5700
  }
3463
5701
  })),
5702
+ ...behavior.commandFields.length ? { commandFields: behavior.commandFields.map((name) => ({
5703
+ name,
5704
+ provenance: {
5705
+ file: emit(app.root),
5706
+ by: "ace-commands"
5707
+ }
5708
+ })) } : {},
3464
5709
  opaqueRequest: behavior.opaqueRequest,
3465
5710
  outputFields: behavior.outputFields.map((name) => ({
3466
5711
  name,
@@ -3477,6 +5722,9 @@ async function analyze(root, options = {}) {
3477
5722
  }
3478
5723
  })),
3479
5724
  transformedStores: behavior.transformedStores,
5725
+ delivered: behavior.delivered,
5726
+ pageReads: behavior.pageReads,
5727
+ unreadablePages: behavior.unreadablePages,
3480
5728
  outputReads: behavior.outputReads,
3481
5729
  trace: behavior.trace.map((step) => ({
3482
5730
  ...step,
@@ -3505,6 +5753,7 @@ async function analyze(root, options = {}) {
3505
5753
  entryPoints,
3506
5754
  behaviors,
3507
5755
  jsonSchemas,
5756
+ jobs: collectJobs(app),
3508
5757
  writtenAnywhere: analyzer.writtenAnywhere(),
3509
5758
  addressedAnywhere: analyzer.addressedAnywhere(),
3510
5759
  seededAnywhere: analyzer.seededAnywhere()