@filipebraida/adonis-function-points 0.6.0 → 0.7.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.
@@ -1,4 +1,4 @@
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-DaU4uAqT.js";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Node, Project, SyntaxKind } from "ts-morph";
@@ -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) {
@@ -1194,50 +1220,858 @@ function isNoise(call, owner) {
1194
1220
  * costs nothing and is not the same as silencing an unresolved call: nothing
1195
1221
  * was ever there to resolve.
1196
1222
  */
1197
- function isIterationCall(call) {
1198
- const expression = call.getExpression();
1199
- if (!Node.isPropertyAccessExpression(expression)) return false;
1200
- return isIteration(expression.getName(), call);
1223
+ function isIterationCall(call) {
1224
+ const expression = call.getExpression();
1225
+ if (!Node.isPropertyAccessExpression(expression)) return false;
1226
+ return isIteration(expression.getName(), call);
1227
+ }
1228
+ /** An iteration method whose first argument is an inline callback. */
1229
+ function isIteration(method, call) {
1230
+ if (!ITERATION_METHODS.has(method)) return false;
1231
+ const first = call.getArguments()[0];
1232
+ return first !== void 0 && (Node.isArrowFunction(first) || Node.isFunctionExpression(first));
1233
+ }
1234
+ /** Does the receiver resolve to a built-in, by its declaration? */
1235
+ function isNativeReceiver(receiver, owner) {
1236
+ if (Node.isArrayLiteralExpression(receiver) || Node.isStringLiteral(receiver)) return true;
1237
+ if (Node.isPropertyAccessExpression(receiver) && receiver.getExpression().getKind()) {
1238
+ const inner = receiver.getExpression();
1239
+ if (Node.isThisExpression(inner) && owner) return isNativeProperty(owner, receiver.getName());
1240
+ }
1241
+ return false;
1242
+ }
1243
+ function isNativeProperty(owner, name) {
1244
+ /**
1245
+ * Both shapes declare a field: a class property, and a constructor parameter
1246
+ * property. Reading only the first missed every `Map` handed in through the
1247
+ * constructor, which is how a transformer usually receives its lookups.
1248
+ */
1249
+ const property = owner.getProperty(name) ?? owner.getConstructors()[0]?.getParameters().find((parameter) => parameter.getName() === name);
1250
+ if (!property) return false;
1251
+ const typeNode = property.getTypeNode()?.getText();
1252
+ if (typeNode && NATIVE_TYPES.has(typeNode.replace(/<.*/, "").trim())) return true;
1253
+ const initializer = property.getInitializer();
1254
+ if (initializer && Node.isNewExpression(initializer)) return NATIVE_TYPES.has(initializer.getExpression().getText());
1255
+ return initializer !== void 0 && (Node.isArrayLiteralExpression(initializer) || Node.isStringLiteral(initializer));
1256
+ }
1257
+ /**
1258
+ * The body-not-found path: a symbol resolved to an application file whose
1259
+ * member is not there. For a framework service that is expected — the
1260
+ * implementation is in the package — and says nothing about tracing quality.
1261
+ */
1262
+ function isNoiseMember(file, member) {
1263
+ if (member && NEVER_DATA_METHODS.has(member)) return true;
1264
+ const base = file.split("/").pop()?.replace(/\.[jt]s$/, "");
1265
+ return base !== void 0 && FRAMEWORK_SERVICES.has(base);
1266
+ }
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 = 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
+ if (RENDERERS.has(receiver) && RENDER_METHODS.has(method)) payload = call.getArguments()[1];
1440
+ else if (receiver === "response" && RESPONSE_METHODS.has(method)) payload = call.getArguments()[0];
1441
+ else if (isPrinter(callee.getExpression(), ctx.body)) {
1442
+ any = true;
1443
+ seen.add(call);
1444
+ for (const argument of call.getArguments()) classify(unwrap$1(argument), "", ctx, deliveries, 0);
1445
+ continue;
1446
+ } else continue;
1447
+ any = true;
1448
+ if (!payload) continue;
1449
+ seen.add(call);
1450
+ classify(unwrap$1(payload), "", ctx, deliveries, 0);
1451
+ }
1452
+ for (const statement of ctx.body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
1453
+ if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== ctx.body) continue;
1454
+ const value = unwrap$1(statement.getExpression());
1455
+ if (!value) continue;
1456
+ if (Node.isCallExpression(value)) {
1457
+ if (seen.has(value)) continue;
1458
+ /**
1459
+ * `return response.redirect().toRoute(…)`, `return response.noContent()`,
1460
+ * `return inertia.render(…)` (seen above): a call whose chain is rooted at
1461
+ * the response or a renderer is the response itself, not a value handed
1462
+ * back — nothing to classify.
1463
+ */
1464
+ const root = chainRootOf(value);
1465
+ if (root && (RENDERERS.has(root) || root === "response")) continue;
1466
+ }
1467
+ anyReturn = true;
1468
+ classify(value, "", ctx, returns, 0);
1469
+ }
1470
+ return {
1471
+ calls: deliveries,
1472
+ anyCall: any,
1473
+ returns,
1474
+ anyReturn
1475
+ };
1476
+ }
1477
+ /** the identifier a call chain is rooted at: `response.redirect().toRoute(x)` -> 'response' */
1478
+ function chainRootOf(node) {
1479
+ let current = node;
1480
+ for (let depth = 0; current && depth < 40; depth++) {
1481
+ if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current)) {
1482
+ current = current.getExpression();
1483
+ continue;
1484
+ }
1485
+ if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current)) {
1486
+ current = current.getExpression();
1487
+ continue;
1488
+ }
1489
+ return Node.isIdentifier(current) ? current.getText() : Node.isThisExpression(current) ? "this" : null;
1490
+ }
1491
+ return null;
1492
+ }
1493
+ /**
1494
+ * `this.ui.table().row(x)`, `this.logger.info(x)`, `console.log(x)`: a chain rooted
1495
+ * at a printer — through a variable too (`const table = this.ui.table(); table.row(x)`).
1496
+ */
1497
+ function isPrinter(receiver, body, depth = 0) {
1498
+ let current = receiver;
1499
+ for (let steps = 0; current && steps < 20; steps++) {
1500
+ if (Node.isCallExpression(current)) {
1501
+ current = current.getExpression();
1502
+ continue;
1503
+ }
1504
+ if (Node.isPropertyAccessExpression(current)) {
1505
+ const inner = current.getExpression();
1506
+ if (Node.isThisExpression(inner)) return PRINTERS.has(current.getName());
1507
+ current = inner;
1508
+ continue;
1509
+ }
1510
+ if (!Node.isIdentifier(current)) return false;
1511
+ if (current.getText() === "console") return true;
1512
+ const bound = depth < 3 ? bindingOf(current.getText(), body) : null;
1513
+ return !!bound && isPrinter(bound.initializer, body, depth + 1);
1514
+ }
1515
+ return false;
1516
+ }
1517
+ /** `ctx.inertia` -> 'inertia', `inertia` -> 'inertia', `this.response` -> 'response' */
1518
+ function lastSegmentOf(node) {
1519
+ if (Node.isPropertyAccessExpression(node)) return node.getName();
1520
+ return node.getText();
1521
+ }
1522
+ function classify(value, path, ctx, out, depth) {
1523
+ if (!value || depth > 6) return;
1524
+ if (value.getKind() === SyntaxKind.NullKeyword) return;
1525
+ if (Node.isIdentifier(value) && value.getText() === "undefined") return;
1526
+ /**
1527
+ * A constant with no key — `this.logger.info('done')`, a table's `head(['Nome'])`
1528
+ * — is a label or a message, not a field: AFP counts no message DET (§6). Keyed,
1529
+ * `{ titulo: 'X' }` is a value the page receives, and stays one.
1530
+ */
1531
+ if (!path && (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value) || Node.isNumericLiteral(value))) return;
1532
+ if (Node.isObjectLiteralExpression(value)) {
1533
+ for (const property of value.getProperties()) if (Node.isShorthandPropertyAssignment(property)) classify(property.getNameNode(), join(path, property.getName()), ctx, out, depth + 1);
1534
+ else if (Node.isPropertyAssignment(property)) {
1535
+ const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
1536
+ classify(unwrap$1(property.getInitializer()), join(path, name), ctx, out, depth + 1);
1537
+ } else if (Node.isSpreadAssignment(property)) classify(unwrap$1(property.getExpression()), path, ctx, out, depth + 1);
1538
+ else out.push({
1539
+ kind: "scalar",
1540
+ path: join(path, property.getName?.() ?? "*")
1541
+ });
1542
+ return;
1543
+ }
1544
+ if (Node.isConditionalExpression(value)) {
1545
+ classify(unwrap$1(value.getWhenTrue()), path, ctx, out, depth + 1);
1546
+ classify(unwrap$1(value.getWhenFalse()), path, ctx, out, depth + 1);
1547
+ return;
1548
+ }
1549
+ if (Node.isArrayLiteralExpression(value)) {
1550
+ for (const element of value.getElements()) classify(unwrap$1(Node.isSpreadElement(element) ? element.getExpression() : element), path, ctx, out, depth + 1);
1551
+ return;
1552
+ }
1553
+ if (Node.isElementAccessExpression(value)) {
1554
+ classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
1555
+ return;
1556
+ }
1557
+ if (Node.isIdentifier(value)) {
1558
+ const name = value.getText();
1559
+ if (ctx.symbols.has(name)) {
1560
+ out.push({
1561
+ kind: "store",
1562
+ store: ctx.symbols.get(name),
1563
+ path
1564
+ });
1565
+ return;
1566
+ }
1567
+ const bound = bindingOf(name, ctx.body);
1568
+ if (bound) {
1569
+ if (bound.pick && Node.isCallExpression(bound.initializer)) {
1570
+ classifyCall(bound.initializer, path, ctx, out, depth + 1, bound.pick);
1571
+ return;
1572
+ }
1573
+ if (bound.pick && Node.isObjectLiteralExpression(bound.initializer)) {
1574
+ const picked = bound.initializer.getProperty(bound.pick);
1575
+ if (picked && Node.isPropertyAssignment(picked)) {
1576
+ classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
1577
+ return;
1578
+ }
1579
+ if (picked && Node.isShorthandPropertyAssignment(picked)) {
1580
+ classify(picked.getNameNode(), path, ctx, out, depth + 1);
1581
+ return;
1582
+ }
1583
+ }
1584
+ classify(bound.initializer, path, ctx, out, depth + 1);
1585
+ return;
1586
+ }
1587
+ if (isEchoBinding(name, ctx.body)) {
1588
+ out.push({
1589
+ kind: "echo",
1590
+ path
1591
+ });
1592
+ return;
1593
+ }
1594
+ out.push({
1595
+ kind: "scalar",
1596
+ path
1597
+ });
1598
+ return;
1599
+ }
1600
+ if (Node.isCallExpression(value)) {
1601
+ classifyCall(value, path, ctx, out, depth);
1602
+ return;
1603
+ }
1604
+ if (Node.isPropertyAccessExpression(value) || Node.isElementAccessExpression(value)) {
1605
+ classifyAccess(value, path, ctx, out, depth);
1606
+ return;
1607
+ }
1608
+ if (Node.isAwaitExpression(value)) {
1609
+ classify(unwrap$1(value.getExpression()), path, ctx, out, depth + 1);
1610
+ return;
1611
+ }
1612
+ if (Node.isBinaryExpression(value)) {
1613
+ const operator = value.getOperatorToken().getKind();
1614
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
1615
+ classify(unwrap$1(value.getLeft()), path, ctx, out, depth + 1);
1616
+ return;
1617
+ }
1618
+ if (operator === SyntaxKind.AmpersandAmpersandToken) {
1619
+ classify(unwrap$1(value.getRight()), path, ctx, out, depth + 1);
1620
+ return;
1621
+ }
1622
+ }
1623
+ if (Node.isTemplateExpression(value)) {
1624
+ for (const span of value.getTemplateSpans()) classify(unwrap$1(span.getExpression()), path, ctx, out, depth + 1);
1625
+ return;
1626
+ }
1627
+ out.push({
1628
+ kind: "scalar",
1629
+ path
1630
+ });
1631
+ }
1632
+ function classifyCall(value, path, ctx, out, depth, pick) {
1633
+ {
1634
+ const callee = value.getExpression();
1635
+ const method = Node.isPropertyAccessExpression(callee) ? callee.getName() : "";
1636
+ if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(method) && isRequestLike(callee.getExpression())) {
1637
+ out.push({
1638
+ kind: "echo",
1639
+ path
1640
+ });
1641
+ return;
1642
+ }
1643
+ const mapped = mappedLiteralOf(value);
1644
+ if (mapped) {
1645
+ classify(mapped, path, ctx, out, depth + 1);
1646
+ return;
1647
+ }
1648
+ if (method === "map" && Node.isPropertyAccessExpression(callee)) {
1649
+ classifyMapped(value, callee.getExpression(), path, ctx, out, depth);
1650
+ return;
1651
+ }
1652
+ if (Node.isPropertyAccessExpression(callee) && PASSES_THROUGH.has(method) && value.getArguments().length === 0) {
1653
+ classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
1654
+ return;
1655
+ }
1656
+ if (method === "getMeta" && value.getArguments().length === 0) {
1657
+ if (pick) out.push({
1658
+ kind: "scalar",
1659
+ path
1660
+ });
1661
+ else for (const key of PAGINATOR_META) out.push({
1662
+ kind: "scalar",
1663
+ path: join(path, key)
1664
+ });
1665
+ return;
1666
+ }
1667
+ if (Node.isPropertyAccessExpression(callee) && SAME_COLLECTION.has(method)) {
1668
+ classify(unwrap$1(callee.getExpression()), path, ctx, out, depth + 1);
1669
+ return;
1670
+ }
1671
+ if (Node.isPropertyAccessExpression(callee) && lastSegmentOf(callee.getExpression()) === "colors") {
1672
+ const coloured = unwrap$1(value.getArguments()[0]);
1673
+ if (coloured) classify(coloured, path, ctx, out, depth + 1);
1674
+ return;
1675
+ }
1676
+ if (Node.isPropertyAccessExpression(callee) && INERTIA_LAZY.has(method) && RENDERERS.has(lastSegmentOf(callee.getExpression()))) {
1677
+ const body = callbackValueOf(unwrap$1(value.getArguments()[0]));
1678
+ if (body) classify(body, path, ctx, out, depth + 1);
1679
+ else out.push({
1680
+ kind: "scalar",
1681
+ path
1682
+ });
1683
+ return;
1684
+ }
1685
+ const args = [];
1686
+ for (const argument of value.getArguments()) classify(unwrap$1(argument), path, ctx, args, depth + 1);
1687
+ /** only what carries rows matters for a fallback: a scalar argument is a parameter, not an output */
1688
+ const carried = args.filter((item) => item.kind === "store" || item.kind === "call");
1689
+ if (Node.isPropertyAccessExpression(callee)) {
1690
+ const root = chainRootOf(callee.getExpression());
1691
+ if (root && SCALAR_SERVICES.has(root)) {
1692
+ out.push({
1693
+ kind: "scalar",
1694
+ path
1695
+ });
1696
+ return;
1697
+ }
1698
+ }
1699
+ const refs = ctx.followed.get(value);
1700
+ if (refs && refs.length > 0) {
1701
+ out.push({
1702
+ kind: "call",
1703
+ refs,
1704
+ path,
1705
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60),
1706
+ args: carried,
1707
+ ...pick ? { pick } : {}
1708
+ });
1709
+ return;
1710
+ }
1711
+ const access = detectAccess(value, ctx.symbols, ctx.relations);
1712
+ if (access) {
1713
+ out.push({
1714
+ kind: "store",
1715
+ store: access.store,
1716
+ path
1717
+ });
1718
+ if (access.viaRelation) out.push({
1719
+ kind: "store",
1720
+ store: access.viaRelation,
1721
+ path
1722
+ });
1723
+ return;
1724
+ }
1725
+ if (Node.isIdentifier(callee) && NATIVE_GLOBALS.has(callee.getText())) {
1726
+ out.push({
1727
+ kind: "scalar",
1728
+ path
1729
+ });
1730
+ return;
1731
+ }
1732
+ if (Node.isPropertyAccessExpression(callee) && Node.isIdentifier(callee.getExpression()) && NATIVE_GLOBALS.has(callee.getExpression().getText())) {
1733
+ out.push({
1734
+ kind: "scalar",
1735
+ path
1736
+ });
1737
+ return;
1738
+ }
1739
+ /**
1740
+ * A call nobody followed, over rows: a package's CSV builder, a formatter. The
1741
+ * document it builds carries what was handed into it, so the rows' stores leave.
1742
+ * With nothing flowing in there is nothing to say: one DET, opaque, reported.
1743
+ */
1744
+ if (carried.length > 0) {
1745
+ out.push(...carried);
1746
+ return;
1747
+ }
1748
+ /**
1749
+ * `startDate?.toISOString()`, `categoria.trim()`: a call ON a value the body
1750
+ * holds is that value formatted — an echo stays an echo, a store's field one
1751
+ * field. Only when the root itself resolves to nothing else is the call read
1752
+ * on its own.
1753
+ */
1754
+ if (Node.isPropertyAccessExpression(callee)) {
1755
+ const root = chainRootOf(callee.getExpression());
1756
+ if (root && ctx.symbols.has(root)) {
1757
+ out.push({
1758
+ kind: "scalar",
1759
+ path
1760
+ });
1761
+ return;
1762
+ }
1763
+ if (root && root !== "this") {
1764
+ const rootNode = rootIdentifierOf(callee.getExpression());
1765
+ const held = [];
1766
+ if (rootNode) classify(rootNode, path, ctx, held, depth + 1);
1767
+ if (held.length > 0 && held.every((item) => item.kind === "echo")) {
1768
+ out.push({
1769
+ kind: "echo",
1770
+ path
1771
+ });
1772
+ return;
1773
+ }
1774
+ }
1775
+ }
1776
+ /**
1777
+ * `rows.map(…).join('\n')` in a body whose parameter is `rows`: the whole input
1778
+ * transformed, so the input says what leaves — the caller's arguments decide,
1779
+ * and the body itself says nothing. `noticia.publicadaEm?.toISO()` on that
1780
+ * parameter is one FIELD of it, and stays one value below.
1781
+ */
1782
+ if (Node.isPropertyAccessExpression(callee) && transformsParameter(callee, ctx.body)) {
1783
+ out.push({
1784
+ kind: "opaque",
1785
+ path,
1786
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60)
1787
+ });
1788
+ return;
1789
+ }
1790
+ if (Node.isPropertyAccessExpression(callee)) {
1791
+ /**
1792
+ * `comunicado.enviadoEm!.toISODate()`, `(a ?? b).toRFC2822()`: a FIELD read off
1793
+ * the chain, or an expression, then a method — one value. A chain rooted at
1794
+ * `this` (`this.service.find()`) is a call into a service, and not this.
1795
+ */
1796
+ if (readsField(callee.getExpression())) {
1797
+ out.push({
1798
+ kind: "scalar",
1799
+ path
1800
+ });
1801
+ return;
1802
+ }
1803
+ if (BOOLEAN_METHODS.has(method) || FORMAT_METHODS.has(method)) {
1804
+ out.push({
1805
+ kind: "scalar",
1806
+ path
1807
+ });
1808
+ return;
1809
+ }
1810
+ }
1811
+ if (returnsPrimitive(value)) {
1812
+ out.push({
1813
+ kind: "scalar",
1814
+ path
1815
+ });
1816
+ return;
1817
+ }
1818
+ out.push({
1819
+ kind: "opaque",
1820
+ path,
1821
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60)
1822
+ });
1823
+ }
1824
+ }
1825
+ /**
1826
+ * `xs.map(cb)` with no literal in the callback: what the callback RETURNS,
1827
+ * once — a function passed by reference (`rows.map(paraLinha)`) is a call to
1828
+ * that function over the rows; an expression body (`(m) => new T(m).toObject()`)
1829
+ * is classified as if it were the value; anything else is one repeating attribute.
1830
+ */
1831
+ function classifyMapped(value, receiver, path, ctx, out, depth) {
1832
+ const callback = unwrap$1(value.getArguments()[0]);
1833
+ const refs = ctx.followed.get(value);
1834
+ if (callback && Node.isIdentifier(callback) && refs && refs.length > 0) {
1835
+ const rows = [];
1836
+ classify(unwrap$1(receiver), path, ctx, rows, depth + 1);
1837
+ out.push({
1838
+ kind: "call",
1839
+ refs,
1840
+ path,
1841
+ expression: value.getText().replace(/\s+/g, "").slice(0, 60),
1842
+ args: rows.filter((item) => item.kind === "store" || item.kind === "call")
1843
+ });
1844
+ return;
1845
+ }
1846
+ const body = callbackValueOf(callback);
1847
+ if (body) {
1848
+ classify(body, path, ctx, out, depth + 1);
1849
+ return;
1850
+ }
1851
+ out.push({
1852
+ kind: "scalar",
1853
+ path
1854
+ });
1855
+ }
1856
+ /** the value a callback hands back: an expression body, or the one `return` of a block */
1857
+ function callbackValueOf(callback) {
1858
+ if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) return null;
1859
+ const body = callback.getBody();
1860
+ if (!Node.isBlock(body)) return unwrap$1(body);
1861
+ const returns = body.getStatements().filter(Node.isReturnStatement);
1862
+ return returns.length === 1 ? unwrap$1(returns[0].getExpression()) : null;
1863
+ }
1864
+ /**
1865
+ * Does this chain read a property (not a method) or hold an expression before
1866
+ * the method is applied? `comunicado.enviadoEm.toISO()` does; `rows.map(f).join()`
1867
+ * does not; a chain rooted at `this` is a service, and does not.
1868
+ */
1869
+ function readsField(node) {
1870
+ let current = node;
1871
+ for (let depth = 0; current && depth < 40; depth++) {
1872
+ if (Node.isCallExpression(current)) {
1873
+ const inner = current.getExpression();
1874
+ current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
1875
+ continue;
1876
+ }
1877
+ if (Node.isAwaitExpression(current) || Node.isNonNullExpression(current)) {
1878
+ current = current.getExpression();
1879
+ continue;
1880
+ }
1881
+ if (Node.isParenthesizedExpression(current)) {
1882
+ const inner = unwrap$1(current.getExpression());
1883
+ if (!inner) return false;
1884
+ if (Node.isBinaryExpression(inner) || Node.isConditionalExpression(inner)) return true;
1885
+ current = inner;
1886
+ continue;
1887
+ }
1888
+ if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return chainRootOf(current) !== "this";
1889
+ return false;
1890
+ }
1891
+ return false;
1892
+ }
1893
+ /**
1894
+ * Is this chain a method (or methods) applied to a plain parameter of the body,
1895
+ * with no field read in between? `rows.map(f).join(s)` is; `noticia.capa?.toISO()`
1896
+ * reads a field first and is not.
1897
+ */
1898
+ function transformsParameter(callee, body) {
1899
+ let current = callee.getExpression();
1900
+ for (let depth = 0; current && depth < 40; depth++) {
1901
+ if (Node.isCallExpression(current)) {
1902
+ const inner = current.getExpression();
1903
+ current = Node.isPropertyAccessExpression(inner) ? inner.getExpression() : inner;
1904
+ continue;
1905
+ }
1906
+ if (Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
1907
+ current = current.getExpression();
1908
+ continue;
1909
+ }
1910
+ if (Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current)) return false;
1911
+ if (!Node.isIdentifier(current)) return false;
1912
+ const name = current.getText();
1913
+ return Node.isParametered(body) && body.getParameters().some((p) => Node.isIdentifier(p.getNameNode()) && p.getNameNode().getText() === name);
1914
+ }
1915
+ return false;
1201
1916
  }
1202
- /** An iteration method whose first argument is an inline callback. */
1203
- function isIteration(method, call) {
1204
- if (!ITERATION_METHODS.has(method)) return false;
1205
- const first = call.getArguments()[0];
1206
- return first !== void 0 && (Node.isArrowFunction(first) || Node.isFunctionExpression(first));
1917
+ /** the identifier node a chain is rooted at: `startDate?.toISOString` -> `startDate` */
1918
+ function rootIdentifierOf(node) {
1919
+ let current = node;
1920
+ for (let depth = 0; current && depth < 40; depth++) {
1921
+ if (Node.isCallExpression(current) || Node.isPropertyAccessExpression(current) || Node.isElementAccessExpression(current) || Node.isAwaitExpression(current) || Node.isParenthesizedExpression(current) || Node.isNonNullExpression(current)) {
1922
+ current = current.getExpression();
1923
+ continue;
1924
+ }
1925
+ return Node.isIdentifier(current) ? current : null;
1926
+ }
1927
+ return null;
1207
1928
  }
1208
- /** Does the receiver resolve to a built-in, by its declaration? */
1209
- function isNativeReceiver(receiver, owner) {
1210
- if (Node.isArrayLiteralExpression(receiver) || Node.isStringLiteral(receiver)) return true;
1211
- if (Node.isPropertyAccessExpression(receiver) && receiver.getExpression().getKind()) {
1212
- const inner = receiver.getExpression();
1213
- if (Node.isThisExpression(inner) && owner) return isNativeProperty(owner, receiver.getName());
1929
+ /** the call's declared return type, `Promise<…>` unwrapped, is a boolean, string, number or a union of those */
1930
+ function returnsPrimitive(call) {
1931
+ try {
1932
+ let type = call.getReturnType();
1933
+ if (type.getSymbol()?.getName() === "Promise") type = type.getTypeArguments()[0] ?? type;
1934
+ const primitive = (t) => t.isBoolean() || t.isBooleanLiteral() || t.isString() || t.isStringLiteral() || t.isNumber() || t.isNumberLiteral() || t.isEnumLiteral() || t.isNull() || t.isUndefined();
1935
+ if (type.isUnion()) {
1936
+ const members = type.getUnionTypes();
1937
+ return members.length > 0 && members.every(primitive) && !members.every((t) => t.isNull() || t.isUndefined());
1938
+ }
1939
+ return primitive(type);
1940
+ } catch {
1941
+ return false;
1214
1942
  }
1215
- return false;
1216
1943
  }
1217
- function isNativeProperty(owner, name) {
1944
+ /**
1945
+ * `x.data`, `resultado.linhas`, `rows[0]`: a part of a value the body holds.
1946
+ *
1947
+ * on a variable bound to a followed call that key of what the call returns
1948
+ * on a store-bound variable the store (`rows[0]`, `.data`) or a field (scalar)
1949
+ * on the validated payload or the request an echo of input — counts on entry
1950
+ * anything else one value
1951
+ */
1952
+ function classifyAccess(value, path, ctx, out, depth) {
1953
+ const root = rootSymbolOf(value);
1218
1954
  /**
1219
- * Both shapes declare a field: a class property, and a constructor parameter
1220
- * property. Reading only the first missed every `Map` handed in through the
1221
- * constructor, which is how a transformer usually receives its lookups.
1955
+ * `x.nome`, `x['nome']`: the key named; `x[papel]`: a key the body computes — one
1956
+ * value, unnamed (`*`); `rows[0]`: one row of the collection, the collection.
1222
1957
  */
1223
- const property = owner.getProperty(name) ?? owner.getConstructors()[0]?.getParameters().find((parameter) => parameter.getName() === name);
1224
- if (!property) return false;
1225
- const typeNode = property.getTypeNode()?.getText();
1226
- if (typeNode && NATIVE_TYPES.has(typeNode.replace(/<.*/, "").trim())) return true;
1227
- const initializer = property.getInitializer();
1228
- if (initializer && Node.isNewExpression(initializer)) return NATIVE_TYPES.has(initializer.getExpression().getText());
1229
- return initializer !== void 0 && (Node.isArrayLiteralExpression(initializer) || Node.isStringLiteral(initializer));
1958
+ const property = Node.isPropertyAccessExpression(value) ? value.getName() : keyOfElementAccess(value);
1959
+ if (root && ctx.symbols.has(root)) {
1960
+ if (!property || PASSES_THROUGH.has(property)) out.push({
1961
+ kind: "store",
1962
+ store: ctx.symbols.get(root),
1963
+ path
1964
+ });
1965
+ else out.push({
1966
+ kind: "scalar",
1967
+ path: path || property.replace(/^\*$/, "")
1968
+ });
1969
+ return;
1970
+ }
1971
+ if (root) {
1972
+ const bound = bindingOf(root, ctx.body);
1973
+ if (bound && Node.isCallExpression(bound.initializer)) {
1974
+ if (SCALAR_PROPS.has(property)) {
1975
+ out.push({
1976
+ kind: "scalar",
1977
+ path
1978
+ });
1979
+ return;
1980
+ }
1981
+ /**
1982
+ * `resultado.linhas`: one key of what the call returns; `meta.pagina` on a
1983
+ * destructured `meta`: the key under the key; `rows[0]`, `.data`: the whole.
1984
+ */
1985
+ const own = property && !PASSES_THROUGH.has(property) ? property : void 0;
1986
+ const pick = [bound.pick, own].filter(Boolean).join(".") || void 0;
1987
+ if (own && !ctx.followed.has(bound.initializer)) {
1988
+ out.push({
1989
+ kind: "scalar",
1990
+ path
1991
+ });
1992
+ return;
1993
+ }
1994
+ classifyCall(bound.initializer, path, ctx, out, depth + 1, pick);
1995
+ return;
1996
+ }
1997
+ if (bound && Node.isObjectLiteralExpression(bound.initializer) && property) {
1998
+ const picked = bound.initializer.getProperty(property);
1999
+ if (picked && Node.isPropertyAssignment(picked)) {
2000
+ classify(unwrap$1(picked.getInitializer()), path, ctx, out, depth + 1);
2001
+ return;
2002
+ }
2003
+ }
2004
+ if (isEchoBinding(root, ctx.body) || root === "request" || root === "params") {
2005
+ out.push({
2006
+ kind: "echo",
2007
+ path
2008
+ });
2009
+ return;
2010
+ }
2011
+ }
2012
+ out.push({
2013
+ kind: "scalar",
2014
+ path: path || property.replace(/^\*$/, "")
2015
+ });
2016
+ }
2017
+ function keyOfElementAccess(value) {
2018
+ const argument = unwrap$1(value.getArgumentExpression());
2019
+ if (!argument || Node.isNumericLiteral(argument)) return "";
2020
+ if (Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument)) return argument.getLiteralValue();
2021
+ return "*";
2022
+ }
2023
+ const join = (prefix, name) => prefix ? `${prefix}.${name}` : name;
2024
+ /** `await x` binds x: the value, not the promise */
2025
+ function unwrapAwait$1(node) {
2026
+ let current = unwrap$1(node);
2027
+ while (current && Node.isAwaitExpression(current)) current = unwrap$1(current.getExpression());
2028
+ return current;
1230
2029
  }
1231
2030
  /**
1232
- * The body-not-found path: a symbol resolved to an application file whose
1233
- * member is not there. For a framework service that is expected — the
1234
- * implementation is in the package — and says nothing about tracing quality.
2031
+ * How a local name was bound in this body: `const x = …` gives the initializer;
2032
+ * `const { x, y } = …` gives the initializer and the key picked out of it.
1235
2033
  */
1236
- function isNoiseMember(file, member) {
1237
- if (member && NEVER_DATA_METHODS.has(member)) return true;
1238
- const base = file.split("/").pop()?.replace(/\.[jt]s$/, "");
1239
- return base !== void 0 && FRAMEWORK_SERVICES.has(base);
2034
+ function bindingOf(name, body) {
2035
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
2036
+ const nameNode = declaration.getNameNode();
2037
+ if (Node.isIdentifier(nameNode)) {
2038
+ if (nameNode.getText() !== name) continue;
2039
+ const initializer = unwrapAwait$1(declaration.getInitializer());
2040
+ return initializer ? { initializer } : null;
2041
+ }
2042
+ if (Node.isObjectBindingPattern(nameNode)) {
2043
+ const element = nameNode.getElements().find((e) => e.getName() === name);
2044
+ if (!element) continue;
2045
+ const initializer = unwrapAwait$1(declaration.getInitializer());
2046
+ if (!initializer) return null;
2047
+ return {
2048
+ initializer,
2049
+ pick: element.getPropertyNameNode()?.getText() ?? element.getName()
2050
+ };
2051
+ }
2052
+ }
2053
+ return null;
2054
+ }
2055
+ /**
2056
+ * A name bound by destructuring the validated payload or the request:
2057
+ * `const { busca } = await request.validateUsing(x)`, `const { id } = params`.
2058
+ */
2059
+ function isEchoBinding(name, body) {
2060
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
2061
+ const binding = declaration.getNameNode();
2062
+ if (!Node.isObjectBindingPattern(binding)) continue;
2063
+ if (!binding.getElements().some((element) => element.getName() === name)) continue;
2064
+ const initializer = unwrap$1(declaration.getInitializer());
2065
+ if (!initializer) continue;
2066
+ if (Node.isIdentifier(initializer) && /^(params|request)$/.test(initializer.getText())) return true;
2067
+ if (Node.isCallExpression(initializer)) {
2068
+ const callee = initializer.getExpression();
2069
+ if (Node.isPropertyAccessExpression(callee) && ECHOES_INPUT.has(callee.getName())) return true;
2070
+ }
2071
+ }
2072
+ return false;
1240
2073
  }
2074
+ const isRequestLike = (node) => lastSegmentOf(node) === "request";
1241
2075
  //#endregion
1242
2076
  //#region src/inventory/graph/call_graph.ts
1243
2077
  /**
@@ -1271,7 +2105,7 @@ function validatorFieldsIn(body, file, app) {
1271
2105
  * validator and is usually not exported, so looking for it where the call site
1272
2106
  * is finds nothing — which is how five fields stayed invisible.
1273
2107
  */
1274
- const { leaves, opaque: unreadable } = leavesOf(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
2108
+ const { leaves, opaque: unreadable } = leavesOf$1(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
1275
2109
  const isOpaque = new Set(unreadable);
1276
2110
  for (const leaf of leaves) {
1277
2111
  const field = `${name}.${leaf}`;
@@ -1414,7 +2248,7 @@ function calleeOwner(call) {
1414
2248
  const owner = callee.getExpression();
1415
2249
  return Node.isPropertyAccessExpression(owner) ? owner.getName() : void 0;
1416
2250
  }
1417
- function leavesOf(node, resolveRef) {
2251
+ function leavesOf$1(node, resolveRef) {
1418
2252
  const object = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
1419
2253
  if (!object) return {
1420
2254
  leaves: [],
@@ -1607,9 +2441,10 @@ function createAnalyzer(app, stores, options = {}) {
1607
2441
  const followUps = [];
1608
2442
  const unresolved = [];
1609
2443
  const reads = [];
1610
- /** calls a strategy claimed: a nested transformer's keys arrive through its body */
1611
- const followedCalls = /* @__PURE__ */ new Set();
2444
+ /** calls a strategy claimed, and where they lead: a nested transformer's keys arrive through its body */
2445
+ const followedCalls = /* @__PURE__ */ new Map();
1612
2446
  const validator = validatorFieldsIn(body, file, app);
2447
+ const commandFields = owner && isCommandClass(owner) ? commandFieldsOf(owner) : [];
1613
2448
  const request = requestFieldsIn(body);
1614
2449
  const context = {
1615
2450
  file,
@@ -1652,10 +2487,15 @@ function createAnalyzer(app, stores, options = {}) {
1652
2487
  if (access.mode === "read") {
1653
2488
  const chain = chainShapeOf(call);
1654
2489
  const shape = chain.aggregate ? "aggregate" : chain.selected.length > 0 ? "select" : "whole";
2490
+ /**
2491
+ * The select list survives an aggregate shape: `select('categoria').count()`
2492
+ * with a GROUP BY leaves the grouped column as well as the count. Dropping
2493
+ * it left a summary at 1 DET.
2494
+ */
1655
2495
  const read = (store, how, via) => reads.push({
1656
2496
  store,
1657
2497
  shape: how,
1658
- columns: how === "select" ? chain.selected : [],
2498
+ columns: how === "whole" ? [] : chain.selected,
1659
2499
  ...via ? { via } : {}
1660
2500
  });
1661
2501
  if (access.method === "related" && access.viaRelation) read(access.viaRelation, shape);
@@ -1709,7 +2549,7 @@ function createAnalyzer(app, stores, options = {}) {
1709
2549
  by: resolved.by,
1710
2550
  technical
1711
2551
  });
1712
- followedCalls.add(call);
2552
+ followedCalls.set(call, resolved.refs);
1713
2553
  continue;
1714
2554
  }
1715
2555
  if (isWorthReporting(call, symbols, imports) && !isNoise(call, owner)) unresolved.push({
@@ -1723,12 +2563,21 @@ function createAnalyzer(app, stores, options = {}) {
1723
2563
  * Read after the loop: whether a key holds a nested transformer is known only
1724
2564
  * once the strategies have said which calls they follow.
1725
2565
  */
1726
- const output = outputFieldsIn(body, owner, storesByName, (c) => followedCalls.has(c));
2566
+ const followed = (c) => followedCalls.has(c);
2567
+ const output = outputFieldsIn(body, owner, storesByName, followed);
2568
+ const deliveries = deliveriesIn({
2569
+ body,
2570
+ file,
2571
+ symbols,
2572
+ relations: relationsByStore,
2573
+ followed: followedCalls
2574
+ });
1727
2575
  return {
1728
2576
  accesses,
1729
2577
  followUps,
1730
2578
  unresolved,
1731
2579
  validators: validator.fields,
2580
+ commandFields,
1732
2581
  opaqueValidators: validator.opaque,
1733
2582
  requestFields: request.fields,
1734
2583
  opaqueRequest: request.opaque,
@@ -1736,6 +2585,7 @@ function createAnalyzer(app, stores, options = {}) {
1736
2585
  opaqueOutputs: output.opaqueOutputs,
1737
2586
  transformed: output.resource,
1738
2587
  reads,
2588
+ deliveries,
1739
2589
  bodyHash: hashOf(body)
1740
2590
  };
1741
2591
  }
@@ -1835,18 +2685,137 @@ function createAnalyzer(app, stores, options = {}) {
1835
2685
  const touches = /* @__PURE__ */ new Set();
1836
2686
  const writtenStores = /* @__PURE__ */ new Set();
1837
2687
  const inputFields = /* @__PURE__ */ new Set();
2688
+ const commandFields = /* @__PURE__ */ new Set();
1838
2689
  const opaqueInputFields = /* @__PURE__ */ new Set();
1839
2690
  const requestFields = /* @__PURE__ */ new Set();
1840
2691
  let opaqueRequest = false;
1841
2692
  const outputFields = /* @__PURE__ */ new Set();
1842
2693
  const opaqueOutputFields = /* @__PURE__ */ new Set();
1843
2694
  const transformedStores = /* @__PURE__ */ new Set();
2695
+ const deliveredFields = /* @__PURE__ */ new Set();
2696
+ const deliveredOpaque = /* @__PURE__ */ new Set();
2697
+ const deliveredStores = /* @__PURE__ */ new Set();
2698
+ let anyDelivery = false;
1844
2699
  const outputReads = /* @__PURE__ */ new Map();
1845
2700
  const trace = [];
1846
2701
  const scope = [];
1847
2702
  const unresolved = [];
1848
2703
  const visited = /* @__PURE__ */ new Set();
1849
2704
  let writes = false;
2705
+ /** what a followed body reads, itself and through what it follows — bounded like the walk */
2706
+ const storesReadBy = (ref, depth, seen = /* @__PURE__ */ new Set()) => {
2707
+ const found = /* @__PURE__ */ new Set();
2708
+ const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
2709
+ if (seen.has(key) || depth > maxDepth) return found;
2710
+ seen.add(key);
2711
+ const facts = factsFor(ref);
2712
+ if (!facts) return found;
2713
+ for (const access of facts.accesses) found.add(access.store);
2714
+ for (const followUp of facts.followUps) for (const store of storesReadBy(followUp.ref, depth + 1, seen)) found.add(store);
2715
+ return found;
2716
+ };
2717
+ /**
2718
+ * A delivered call hands on what its body RETURNS — classified, so a returned
2719
+ * `{ data: rows, meta }` delivers the rows' store and the meta's leaves, not
2720
+ * one DET per key. `pick` keeps one key of it: `const { data } = …`,
2721
+ * `resultado.linhas`. Bounded by depth like the walk, and by a seen set so a
2722
+ * body returning itself cannot loop.
2723
+ */
2724
+ const deliver = (item, depth, seen = /* @__PURE__ */ new Set()) => {
2725
+ switch (item.kind) {
2726
+ case "store":
2727
+ deliveredStores.add(item.store);
2728
+ return;
2729
+ case "scalar":
2730
+ deliveredFields.add(item.path || "<value>");
2731
+ return;
2732
+ case "echo": return;
2733
+ case "opaque":
2734
+ deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
2735
+ return;
2736
+ case "call": {
2737
+ if (depth > maxDepth + 2) return;
2738
+ let resolved = false;
2739
+ for (const ref of item.refs) {
2740
+ const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}#${item.pick ?? ""}#${item.path}`;
2741
+ if (seen.has(key)) continue;
2742
+ seen.add(key);
2743
+ const facts = factsFor(ref);
2744
+ if (!facts) continue;
2745
+ if (facts.outputs.length > 0 || facts.transformed) {
2746
+ resolved = true;
2747
+ continue;
2748
+ }
2749
+ const returned = facts.deliveries.returns.filter((r) => !item.pick || r.path === item.pick || r.path.startsWith(`${item.pick}.`));
2750
+ /**
2751
+ * `egresso.curso` where the body returns the row itself (`return
2752
+ * Egresso.query()…first()`, path ''): the pick lands INSIDE a returned
2753
+ * value — one field of a store is one value; one key of a returned call
2754
+ * is that call picked deeper.
2755
+ */
2756
+ if (item.pick && returned.length === 0) {
2757
+ const above = facts.deliveries.returns.filter((r) => r.path === "" || item.pick.startsWith(`${r.path}.`));
2758
+ for (const r of above) {
2759
+ const rest = r.path ? item.pick.slice(r.path.length + 1) : item.pick;
2760
+ if (r.kind === "call") deliver({
2761
+ ...r,
2762
+ path: item.path,
2763
+ pick: rest
2764
+ }, depth + 1, seen);
2765
+ else if (r.kind === "store" || r.kind === "scalar") deliveredFields.add(item.path || "<value>");
2766
+ }
2767
+ if (above.length > 0) {
2768
+ resolved = true;
2769
+ continue;
2770
+ }
2771
+ }
2772
+ /**
2773
+ * A return the classifier could not read at all (`rows.map(…).join(…)`)
2774
+ * says nothing about what leaves; what the body read, or what was handed
2775
+ * into it, says more — so it does not count as resolved.
2776
+ */
2777
+ if (returned.length > 0 && returned.some((r) => r.kind !== "opaque")) {
2778
+ for (const r of returned) {
2779
+ const rest = item.pick ? r.path.slice(item.pick.length).replace(/^\./, "") : r.path;
2780
+ const path = [item.path, rest].filter(Boolean).join(".");
2781
+ deliver({
2782
+ ...r,
2783
+ path
2784
+ }, depth + 1, seen);
2785
+ }
2786
+ resolved = true;
2787
+ continue;
2788
+ }
2789
+ if (item.pick) continue;
2790
+ const read = storesReadBy(ref, depth + 1);
2791
+ if (read.size > 0) {
2792
+ for (const store of read) deliveredStores.add(store);
2793
+ resolved = true;
2794
+ }
2795
+ }
2796
+ if (resolved) return;
2797
+ /**
2798
+ * `const { confidenciais } = await this.contar()` where the body's return
2799
+ * is unreadable: one KEY of it, named — one value, not an opaque floor.
2800
+ * Only a key that carries rows on (`data`, `rows`) stays unreadable.
2801
+ */
2802
+ if (item.pick && !PASSES_ROWS.has(item.pick.split(".")[0])) {
2803
+ deliveredFields.add(item.path || "<value>");
2804
+ return;
2805
+ }
2806
+ /**
2807
+ * The body returned no literal and read no store — a CSV builder, a
2808
+ * formatter over rows handed in. The document it built carries what it
2809
+ * received, so the rows' stores leave. Nothing handed in: opaque.
2810
+ */
2811
+ if (item.args.length > 0) {
2812
+ for (const argument of item.args) deliver(argument, depth, seen);
2813
+ return;
2814
+ }
2815
+ deliveredOpaque.add(`${item.path ? `${item.path}.` : ""}<${item.expression}>`);
2816
+ }
2817
+ }
2818
+ };
1850
2819
  const visit = (ref, depth, technical = false) => {
1851
2820
  const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
1852
2821
  if (visited.has(key) || depth > maxDepth) return;
@@ -1885,12 +2854,20 @@ function createAnalyzer(app, stores, options = {}) {
1885
2854
  }
1886
2855
  unresolved.push(...facts.unresolved);
1887
2856
  for (const field of facts.validators) inputFields.add(field);
2857
+ for (const field of facts.commandFields) commandFields.add(field);
1888
2858
  for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
1889
2859
  for (const field of facts.requestFields) requestFields.add(field);
1890
2860
  if (facts.opaqueRequest) opaqueRequest = true;
1891
2861
  for (const field of facts.outputs) outputFields.add(field);
1892
2862
  for (const field of facts.opaqueOutputs) opaqueOutputFields.add(field);
1893
2863
  if (facts.transformed) transformedStores.add(facts.transformed);
2864
+ /**
2865
+ * Deliveries, resolved here because a delivered CALL leads to a body only
2866
+ * the graph knows: what that body returns is what the value hands on.
2867
+ */
2868
+ const items = [...facts.deliveries.calls, ...depth === 0 ? facts.deliveries.returns : []];
2869
+ if (facts.deliveries.anyCall || depth === 0 && facts.deliveries.anyReturn) anyDelivery = true;
2870
+ for (const item of items) deliver(item, depth);
1894
2871
  for (const { store, shape, columns, via } of facts.reads) {
1895
2872
  const known = outputReads.get(store) ?? {
1896
2873
  whole: false,
@@ -1931,12 +2908,19 @@ function createAnalyzer(app, stores, options = {}) {
1931
2908
  touches: [...touches].sort(),
1932
2909
  writtenStores: [...writtenStores].sort(),
1933
2910
  inputFields: [...inputFields].sort(),
2911
+ commandFields: [...commandFields].sort(),
1934
2912
  opaqueInputFields: [...opaqueInputFields].sort(),
1935
2913
  requestFields: [...requestFields].sort(),
1936
2914
  opaqueRequest,
1937
2915
  outputFields: [...outputFields].sort(),
1938
2916
  opaqueOutputFields: [...opaqueOutputFields].sort(),
1939
2917
  transformedStores: [...transformedStores].sort(),
2918
+ delivered: {
2919
+ any: anyDelivery,
2920
+ fields: [...deliveredFields].sort(),
2921
+ opaqueFields: [...deliveredOpaque].sort(),
2922
+ stores: [...deliveredStores].sort()
2923
+ },
1940
2924
  outputReads: Object.fromEntries([...outputReads.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([store, read]) => [store, {
1941
2925
  whole: read.whole,
1942
2926
  selected: [...read.selected].sort(),
@@ -1969,6 +2953,8 @@ function findBody(file, ref) {
1969
2953
  }
1970
2954
  const fn = file.getFunction(ref.member);
1971
2955
  if (fn) return fn;
2956
+ const initializer = file.getVariableDeclaration(ref.member)?.getInitializer();
2957
+ if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) return initializer;
1972
2958
  return null;
1973
2959
  }
1974
2960
  for (const cls of file.getClasses()) {
@@ -1997,6 +2983,27 @@ function storeSymbolsFor(body, file, app, stores) {
1997
2983
  if (stores.has(named.getName())) symbols.set(binding, named.getName());
1998
2984
  }
1999
2985
  }
2986
+ /**
2987
+ * `const { default: Noticia } = await import('#noticias/models/noticia')`: a
2988
+ * model imported INSIDE the body — an ace command does this to keep the app
2989
+ * from booting for `--help`. The store is the same; only the binding moved.
2990
+ */
2991
+ const storeByFile = /* @__PURE__ */ new Map();
2992
+ for (const [name, store] of stores) storeByFile.set(toPosix(store.provenance.file), name);
2993
+ for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
2994
+ const binding = declaration.getNameNode();
2995
+ if (!Node.isObjectBindingPattern(binding)) continue;
2996
+ let initializer = declaration.getInitializer();
2997
+ if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
2998
+ if (!initializer || !Node.isCallExpression(initializer)) continue;
2999
+ if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
3000
+ const specifier = initializer.getArguments()[0];
3001
+ if (!specifier || !Node.isStringLiteral(specifier)) continue;
3002
+ const target = app.resolveSpecifier(specifier.getLiteralValue());
3003
+ const store = target ? storeByFile.get(toPosix(target)) : void 0;
3004
+ if (!store) continue;
3005
+ for (const element of binding.getElements()) if ((element.getPropertyNameNode()?.getText() ?? element.getName()) === "default") symbols.set(element.getName(), store);
3006
+ }
2000
3007
  for (const declaration of body.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
2001
3008
  const initializer = declaration.getInitializer();
2002
3009
  const name = declaration.getNameNode();
@@ -2162,6 +3169,32 @@ function importMapsOf(file, app) {
2162
3169
  if (alias) exportedAs.set(alias, named.getName());
2163
3170
  }
2164
3171
  }
3172
+ /**
3173
+ * `const { default: SincronizarBulk } = await import('#inpi/actions/sincronizar_bulk')`
3174
+ * `const { execucaoEmAndamento } = await import('#inpi/services/execucao')`
3175
+ *
3176
+ * A module imported INSIDE a body — the shape ace commands use so `--help` does
3177
+ * not boot the application. The binding moved; the body it names did not, and
3178
+ * a command importing its whole action layer this way reached nothing.
3179
+ */
3180
+ for (const declaration of file.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
3181
+ const binding = declaration.getNameNode();
3182
+ if (!Node.isObjectBindingPattern(binding)) continue;
3183
+ let initializer = declaration.getInitializer();
3184
+ if (initializer && Node.isAwaitExpression(initializer)) initializer = initializer.getExpression();
3185
+ if (!initializer || !Node.isCallExpression(initializer)) continue;
3186
+ if (initializer.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
3187
+ const specifier = initializer.getArguments()[0];
3188
+ if (!specifier || !Node.isStringLiteral(specifier)) continue;
3189
+ const target = app.resolveSpecifier(specifier.getLiteralValue());
3190
+ if (!target) continue;
3191
+ for (const element of binding.getElements()) {
3192
+ const exported = element.getPropertyNameNode()?.getText() ?? element.getName();
3193
+ const local = element.getName();
3194
+ imports.set(local, target);
3195
+ if (exported !== "default" && exported !== local) exportedAs.set(local, exported);
3196
+ }
3197
+ }
2165
3198
  return {
2166
3199
  imports,
2167
3200
  exportedAs
@@ -2215,6 +3248,154 @@ function hashOf(body) {
2215
3248
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
2216
3249
  }
2217
3250
  //#endregion
3251
+ //#region src/inventory/sources/jobs.ts
3252
+ /** `X.schedule({}).cron(…)`: a scheduler's way of dispatching */
3253
+ const SCHEDULE_METHODS = new Set(["schedule"]);
3254
+ function collectJobs(app) {
3255
+ const project = new Project({
3256
+ skipAddingFilesFromTsConfig: true,
3257
+ skipFileDependencyResolution: true,
3258
+ compilerOptions: { allowJs: false }
3259
+ });
3260
+ const root = toPosix(app.root);
3261
+ for (const dir of app.scanRoots) project.addSourceFilesAtPaths(`${toPosix(dir)}/**/*.ts`);
3262
+ project.addSourceFilesAtPaths(`${root}/start/**/*.ts`);
3263
+ project.addSourceFilesAtPaths(`${root}/commands/**/*.ts`);
3264
+ const jobs = /* @__PURE__ */ new Map();
3265
+ for (const file of project.getSourceFiles()) {
3266
+ const filePath = toPosix(file.getFilePath());
3267
+ if (!isApplicationCode(app.root, filePath) || !inJobsDirectory(filePath)) continue;
3268
+ for (const cls of file.getClasses()) {
3269
+ const name = cls.getName();
3270
+ if (!name || !looksLikeJob(cls)) continue;
3271
+ jobs.set(filePath, {
3272
+ name,
3273
+ file: filePath,
3274
+ dispatchedFrom: [],
3275
+ scheduledFrom: []
3276
+ });
3277
+ }
3278
+ }
3279
+ if (jobs.size === 0) return [];
3280
+ for (const file of project.getSourceFiles()) {
3281
+ const filePath = toPosix(file.getFilePath());
3282
+ if (/\.(spec|test)\.ts$/.test(file.getBaseName())) continue;
3283
+ const { imports } = importMapsOf(file, app);
3284
+ if (imports.size === 0) continue;
3285
+ for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
3286
+ const callee = call.getExpression();
3287
+ if (!Node.isPropertyAccessExpression(callee)) continue;
3288
+ const method = callee.getName();
3289
+ const scheduled = SCHEDULE_METHODS.has(method);
3290
+ if (!scheduled && !DISPATCH_METHODS.has(method)) continue;
3291
+ const receiver = callee.getExpression();
3292
+ if (!Node.isIdentifier(receiver)) continue;
3293
+ const target = imports.get(receiver.getText());
3294
+ const job = target ? jobs.get(toPosix(target)) : void 0;
3295
+ if (!job) continue;
3296
+ const sites = scheduled ? job.scheduledFrom : job.dispatchedFrom;
3297
+ if (!sites.includes(filePath)) sites.push(filePath);
3298
+ }
3299
+ }
3300
+ return [...jobs.values()].sort((a, b) => a.name.localeCompare(b.name));
3301
+ }
3302
+ const inJobsDirectory = (filePath) => filePath.split("/").slice(0, -1).some((segment) => segment === "jobs" || segment === "job");
3303
+ /** extends something called `…Job`, or declares the method a queue would run */
3304
+ function looksLikeJob(cls) {
3305
+ const parent = cls.getExtends()?.getExpression().getText() ?? "";
3306
+ if (/Job$/.test(parent)) return true;
3307
+ return EXECUTION_METHODS.some((method) => cls.getMethod(method) !== void 0);
3308
+ }
3309
+ //#endregion
3310
+ //#region src/inventory/sources/json_schemas.ts
3311
+ function collectJsonSchemas(app) {
3312
+ const project = new Project({
3313
+ skipAddingFilesFromTsConfig: true,
3314
+ skipFileDependencyResolution: true,
3315
+ compilerOptions: { allowJs: false }
3316
+ });
3317
+ /**
3318
+ * Wider than `scanRoots`, and only here.
3319
+ *
3320
+ * `database/` is excluded from the application roots on purpose: test factories
3321
+ * and migrations contain real persistence calls, and scanning them would turn a
3322
+ * test write into a counted function. But `node ace make:seeder` puts seeders in
3323
+ * `database/seeders`, which is where seed data — and therefore a form's schema —
3324
+ * normally lives. Reading a literal counts nothing, so there is no conflict: the
3325
+ * exclusion protects the call graph, not the schema catalogue.
3326
+ *
3327
+ * Without this, `overrides.detFromSchema` naming a schema declared in a seeder
3328
+ * reported "not declared anywhere in the code" and left the DET count at the
3329
+ * floor — the exact case the override exists for.
3330
+ */
3331
+ for (const root of [...app.scanRoots.map(toPosix), `${toPosix(app.root)}/database`]) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
3332
+ const found = /* @__PURE__ */ new Map();
3333
+ for (const file of project.getSourceFiles()) for (const declaration of file.getVariableDeclarations()) {
3334
+ const literal = unwrap(declaration.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
3335
+ if (!literal || !isJsonSchema(literal)) continue;
3336
+ const leaves = leavesOf(literal);
3337
+ if (leaves.length === 0) continue;
3338
+ found.set(declaration.getName(), {
3339
+ name: declaration.getName(),
3340
+ fields: leaves.length,
3341
+ leaves,
3342
+ provenance: {
3343
+ file: toPosix(file.getFilePath()),
3344
+ line: declaration.getStartLineNumber(),
3345
+ by: "json-schema"
3346
+ }
3347
+ });
3348
+ }
3349
+ return found;
3350
+ }
3351
+ /** `{ … } as const` and `{ … } satisfies X` still hold the literal */
3352
+ function unwrap(node) {
3353
+ if (!node) return void 0;
3354
+ if (Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) return unwrap(node.getExpression());
3355
+ return node;
3356
+ }
3357
+ /**
3358
+ * Recognised by shape, never by name.
3359
+ *
3360
+ * A const called `schema` may be anything; an object declaring `type: 'object'`
3361
+ * with a `properties` map is a JSON Schema whatever it is called.
3362
+ */
3363
+ function isJsonSchema(literal) {
3364
+ return (literal.getProperty("type")?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer()?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue() === "object" && literal.getProperty("properties") !== void 0;
3365
+ }
3366
+ /**
3367
+ * Leaves of a JSON Schema, by the table in counting-decisions §7.
3368
+ *
3369
+ * scalar 1
3370
+ * nested object leaves counted individually
3371
+ * array of scalar 1 (repeating group)
3372
+ * array of object the object's leaves, once
3373
+ * enum / const 1
3374
+ */
3375
+ function leavesOf(literal, prefix = "") {
3376
+ const properties = literal.getProperty("properties")?.asKind(SyntaxKind.PropertyAssignment);
3377
+ const items = literal.getProperty("items")?.asKind(SyntaxKind.PropertyAssignment);
3378
+ if (properties) {
3379
+ const map = properties.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
3380
+ if (!map) return [];
3381
+ return map.getProperties().flatMap((property) => {
3382
+ const assignment = property.asKind(SyntaxKind.PropertyAssignment);
3383
+ if (!assignment) return [];
3384
+ const name = assignment.getName().replace(/['"]/g, "");
3385
+ const path = prefix ? `${prefix}.${name}` : name;
3386
+ const nested = unwrap(assignment.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
3387
+ const inner = nested ? leavesOf(nested, path) : [];
3388
+ return inner.length > 0 ? inner : [path];
3389
+ });
3390
+ }
3391
+ if (items) {
3392
+ const element = unwrap(items.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
3393
+ const inner = element ? leavesOf(element, prefix) : [];
3394
+ return inner.length > 0 ? inner : [prefix];
3395
+ }
3396
+ return [];
3397
+ }
3398
+ //#endregion
2218
3399
  //#region src/albrecht/tables.ts
2219
3400
  /** shared grid: [ref band][DET band] -> complexity */
2220
3401
  const GRID = [
@@ -2757,7 +3938,7 @@ function detsFor(entry, behavior, touched, type, options) {
2757
3938
  counted.add(field);
2758
3939
  sources.push(source);
2759
3940
  };
2760
- for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
3941
+ if (entry.kind === "http") for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
2761
3942
  /**
2762
3943
  * `(opaque)` is not decoration: `detFromSchema` replaces the opaque placeholder
2763
3944
  * with a schema's fields, and it used to assume there was exactly one and that
@@ -2777,6 +3958,11 @@ function detsFor(entry, behavior, touched, type, options) {
2777
3958
  * paid for twice.
2778
3959
  */
2779
3960
  for (const field of behavior.requestFields) add(field, `request:${field}`);
3961
+ for (const field of behavior.commandFields ?? []) {
3962
+ const [kind, ...rest] = field.split(".");
3963
+ const name = rest.join(".");
3964
+ add(name, `${kind === "args" ? "arg" : "flag"}:${name}`);
3965
+ }
2780
3966
  if (type === "EO" || type === "EQ") {
2781
3967
  /**
2782
3968
  * counting-decisions §6, per store, in order of what is visible:
@@ -2812,8 +3998,34 @@ function detsFor(entry, behavior, touched, type, options) {
2812
3998
  }
2813
3999
  }
2814
4000
  for (const field of behavior.outputFields) add(field, `transformer:${field}${opaqueOutputs.has(field) ? " (opaque)" : ""}`);
4001
+ /**
4002
+ * Plan 0.7 §A′ — the delivery is the boundary. When the transaction hands
4003
+ * something to a renderer or a response, THAT is what leaves: derived values
4004
+ * and the leaves of literals a followed body returned count 1 each; a store
4005
+ * leaves only if its rows were handed on, raw or through a query object; a
4006
+ * store read to authorise or to decide contributes nothing to the output. A
4007
+ * field that entered and is echoed back counts once, on entry (§7.3) — the
4008
+ * classifier marks it as an echo. Without a delivery point the rule cannot
4009
+ * apply, and every store read leaves, as before.
4010
+ */
4011
+ const delivered = behavior.delivered;
4012
+ if (delivered.any) {
4013
+ for (const field of delivered.fields) add(`render.${field}`, `render:${field}`);
4014
+ for (const field of delivered.opaqueFields) add(`render.${field}`, `render:${field} (opaque)`);
4015
+ }
4016
+ const deliveredStores = new Set(delivered.stores);
4017
+ for (let changed = true; changed;) {
4018
+ changed = false;
4019
+ for (const [store, read] of Object.entries(behavior.outputReads)) {
4020
+ if (deliveredStores.has(store) || read.direct) continue;
4021
+ if (!read.via.some((parent) => deliveredStores.has(parent))) continue;
4022
+ deliveredStores.add(store);
4023
+ changed = true;
4024
+ }
4025
+ }
2815
4026
  for (const store of touched) {
2816
4027
  const read = behavior.outputReads[store];
4028
+ if (delivered.any && !deliveredStores.has(store) && !covered.has(store)) continue;
2817
4029
  if (read?.aggregate) add(`${store}.<aggregate>`, `aggregate:${store} (a count or an existence check: one scalar)`);
2818
4030
  if (covered.has(store)) continue;
2819
4031
  if (read && read.aggregate && !read.whole && read.selected.length === 0) continue;
@@ -2926,12 +4138,16 @@ const RULESET = "afp";
2926
4138
  * column; system timestamps and `serializeAs: null` columns leaving the DETs;
2927
4139
  * master-detail folded into one data function; identity by table; token tables
2928
4140
  * technical; and opaque declarations reaching every function carrying the origin.
4141
+ * Three in 1.6.0: an output's DETs are what the transaction DELIVERS (the render
4142
+ * props, the response payload, what a command prints) read back to their origin;
4143
+ * a function of the same file and a `.map(fn)` by reference are followed, so
4144
+ * FTRs move; and ace commands are transactions, with flags as input.
2929
4145
  *
2930
4146
  * Without the bump, a baseline saved by the previous version compares cleanly
2931
4147
  * against this one and bills the tool's own improvement as work done. The guard
2932
4148
  * exists for exactly that, and only this constant arms it.
2933
4149
  */
2934
- const RULESET_VERSION = "1.5.0";
4150
+ const RULESET_VERSION = "1.6.0";
2935
4151
  function count(input, options = {}) {
2936
4152
  const warnings = [];
2937
4153
  const usage = usageOf(input);
@@ -3013,7 +4229,8 @@ function count(input, options = {}) {
3013
4229
  const countedRoots = new Set(dataFunctions.map((fn) => fn.name));
3014
4230
  const countedStores = new Map(countable.filter((store) => countedRoots.has(grouping.rootOf.get(store.name) ?? store.name)).map((store) => [store.name, store]));
3015
4231
  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, {
4232
+ const entryPoints = input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? ""));
4233
+ const transactionalFunctions = countTransactionalFunctions(entryPoints, input.behaviors, {
3017
4234
  countedStores,
3018
4235
  grouping,
3019
4236
  root: input.app.root,
@@ -3047,6 +4264,9 @@ function count(input, options = {}) {
3047
4264
  }));
3048
4265
  warnings.push(...unreadableInputWarnings(input));
3049
4266
  warnings.push(...unreadableOutputWarnings(input));
4267
+ warnings.push(...unreadableDeliveryWarnings(input));
4268
+ warnings.push(...commandWarnings(entryPoints, transactionalFunctions));
4269
+ warnings.push(...undispatchedJobWarnings(input));
3050
4270
  warnings.push(...lookAlikeWarnings(functions));
3051
4271
  warnings.push(...seededOnlyWarnings(functions, grouping.members, input.seededAnywhere));
3052
4272
  return {
@@ -3058,6 +4278,67 @@ function count(input, options = {}) {
3058
4278
  };
3059
4279
  }
3060
4280
  /**
4281
+ * Transactions that deliver something the analysis cannot read.
4282
+ *
4283
+ * A generated document (`response.send(gerarCsv(rows))`), a value built by a
4284
+ * call nobody followed. Each counts 1 DET — a floor — and the transaction's
4285
+ * output is understated by whatever the value carries. Named with the
4286
+ * expression, because the fix is usually in the code: return a literal, or a
4287
+ * transformer, and the fields become readable.
4288
+ */
4289
+ function unreadableDeliveryWarnings(input) {
4290
+ const blind = input.entryPoints.map((entry) => ({
4291
+ entry,
4292
+ behavior: input.behaviors.get(entry.id)
4293
+ })).filter(({ behavior }) => behavior && !behavior.writes && behavior.delivered.opaqueFields.length > 0);
4294
+ if (blind.length === 0) return [];
4295
+ return [
4296
+ `${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:`,
4297
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.delivered.opaqueFields.join(", ")}`),
4298
+ ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
4299
+ ];
4300
+ }
4301
+ /**
4302
+ * Jobs no transaction reaches — plan 0.7 §D.
4303
+ *
4304
+ * A job dispatched by a handler is part of that handler's transaction (§9). One
4305
+ * that nothing reachable dispatches is either a scheduled process — an elementary
4306
+ * process nobody is counting — or dead code, and the code cannot say which. It is
4307
+ * reported, never counted: inventing an elementary process is the error this
4308
+ * package exists to avoid.
4309
+ */
4310
+ function undispatchedJobWarnings(input) {
4311
+ if (!input.jobs?.length) return [];
4312
+ const reached = /* @__PURE__ */ new Set();
4313
+ for (const behavior of input.behaviors.values()) for (const step of behavior.trace) reached.add(toPosix(step.file));
4314
+ const orphans = input.jobs.filter((job) => !reached.has(job.file));
4315
+ if (orphans.length === 0) return [];
4316
+ const where = (file) => relativeTo(input.app.root, file);
4317
+ 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 }) => {
4318
+ const at = ` ${name} (${where(file)}): `;
4319
+ if (scheduledFrom.length > 0) return `${at}scheduled from ${scheduledFrom.map(where).join(", ")}, outside every transaction`;
4320
+ if (dispatchedFrom.length > 0) return `${at}dispatched from ${dispatchedFrom.map(where).join(", ")}, which no transaction reaches`;
4321
+ return `${at}dispatched by nothing in the application — dead code, or a scheduler this analysis does not read`;
4322
+ })];
4323
+ }
4324
+ /**
4325
+ * Ace commands counted as transactions — plan 0.7 §C.
4326
+ *
4327
+ * A batch process an operator starts is an elementary process, and it is counted.
4328
+ * Half the commands on the validated applications are development tools — a data
4329
+ * generator, a scaffolder — which the CPM does not count and the code cannot tell
4330
+ * from an importer: both write the same table. So each one is listed with the FP
4331
+ * at stake, the collector's hint beside it, and the way out is a declaration.
4332
+ */
4333
+ function commandWarnings(entryPoints, functions) {
4334
+ const counted = entryPoints.filter((entry) => entry.kind === "command").map((entry) => ({
4335
+ entry,
4336
+ fn: functions.find((f) => f.id === `tx:${entry.identity}`)
4337
+ })).filter(({ fn }) => fn !== void 0);
4338
+ if (counted.length === 0) return [];
4339
+ 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("; ")}` : ""))];
4340
+ }
4341
+ /**
3061
4342
  * Transactions that look like the same elementary process.
3062
4343
  *
3063
4344
  * The CPM counts identical processing logic once. `GET /perfil` and
@@ -3379,7 +4660,9 @@ var CoverageTooLowError = class extends Error {
3379
4660
  async function analyze(root, options = {}) {
3380
4661
  const app = await discoverApp(root);
3381
4662
  const { stores, unresolved: storeProblems } = await collectDataStores(app);
3382
- const { entryPoints, unresolved: routeProblems } = await collectEntryPoints(app);
4663
+ const routes = await collectEntryPoints(app);
4664
+ const routeProblems = routes.unresolved;
4665
+ const entryPoints = [...routes.entryPoints, ...collectCommands(app)];
3383
4666
  /** only read when an override names one — but collected once, like everything else */
3384
4667
  const jsonSchemas = collectJsonSchemas(app);
3385
4668
  const analyzer = createAnalyzer(app, stores, {
@@ -3461,6 +4744,13 @@ async function analyze(root, options = {}) {
3461
4744
  by: "request"
3462
4745
  }
3463
4746
  })),
4747
+ ...behavior.commandFields.length ? { commandFields: behavior.commandFields.map((name) => ({
4748
+ name,
4749
+ provenance: {
4750
+ file: emit(app.root),
4751
+ by: "ace-commands"
4752
+ }
4753
+ })) } : {},
3464
4754
  opaqueRequest: behavior.opaqueRequest,
3465
4755
  outputFields: behavior.outputFields.map((name) => ({
3466
4756
  name,
@@ -3477,6 +4767,7 @@ async function analyze(root, options = {}) {
3477
4767
  }
3478
4768
  })),
3479
4769
  transformedStores: behavior.transformedStores,
4770
+ delivered: behavior.delivered,
3480
4771
  outputReads: behavior.outputReads,
3481
4772
  trace: behavior.trace.map((step) => ({
3482
4773
  ...step,
@@ -3505,6 +4796,7 @@ async function analyze(root, options = {}) {
3505
4796
  entryPoints,
3506
4797
  behaviors,
3507
4798
  jsonSchemas,
4799
+ jobs: collectJobs(app),
3508
4800
  writtenAnywhere: analyzer.writtenAnywhere(),
3509
4801
  addressedAnywhere: analyzer.addressedAnywhere(),
3510
4802
  seededAnywhere: analyzer.seededAnywhere()