@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.
@@ -83,129 +83,6 @@ function isApplicationCode(root, file) {
83
83
  return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
84
84
  }
85
85
  //#endregion
86
- //#region src/inventory/sources/event_bindings.ts
87
- /** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
88
- const LISTENER_METHOD = "handle";
89
- function collectEventBindings(app) {
90
- const project = new Project({
91
- skipAddingFilesFromTsConfig: true,
92
- skipFileDependencyResolution: true,
93
- compilerOptions: { allowJs: false }
94
- });
95
- for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
96
- const bindings = /* @__PURE__ */ new Map();
97
- for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
98
- const expression = call.getExpression();
99
- if (!Node.isPropertyAccessExpression(expression)) continue;
100
- if (expression.getName() !== "on") continue;
101
- const [event, handlers] = call.getArguments();
102
- if (!event || !handlers) continue;
103
- const eventFile = resolveEventClass(event, file, app);
104
- if (!eventFile) continue;
105
- const refs = listenersOf(handlers, file, app);
106
- if (refs.length === 0) continue;
107
- bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
108
- }
109
- return bindings;
110
- }
111
- /**
112
- * The event class a dispatch or a binding names.
113
- *
114
- * Two shapes reach here: the class imported directly, and the generated
115
- * registry (`events.OrderPlaced`), which is what `node ace make:event` produces
116
- * and therefore the common one. Exported because the resolver has to ask the
117
- * same question of a call site, and two implementations of "which event is
118
- * this" would drift.
119
- */
120
- function resolveEventClass(expression, from, app) {
121
- if (Node.isIdentifier(expression)) {
122
- const target = importedFrom(expression.getText(), from, app);
123
- return target ? toPosix(target) : null;
124
- }
125
- if (!Node.isPropertyAccessExpression(expression)) return null;
126
- const root = expression.getExpression();
127
- if (!Node.isIdentifier(root)) return null;
128
- const registry = importedFrom(root.getText(), from, app);
129
- if (!registry) return null;
130
- return registryEntry(registry, expression.getName(), from.getProject(), app);
131
- }
132
- /** listener bodies named by the second argument of `emitter.on` */
133
- function listenersOf(handlers, from, app) {
134
- const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
135
- const refs = [];
136
- for (const entry of entries) {
137
- /**
138
- * `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
139
- * and taking `handle` on faith there would look for a body that is not
140
- * the one bound.
141
- */
142
- if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
143
- const [target, member] = entry.getElements();
144
- const file = target ? listenerFile(target, from, app) : null;
145
- if (!file) continue;
146
- const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
147
- refs.push({
148
- file,
149
- member: named ?? LISTENER_METHOD
150
- });
151
- continue;
152
- }
153
- const file = listenerFile(entry, from, app);
154
- if (file) refs.push({
155
- file,
156
- member: LISTENER_METHOD
157
- });
158
- }
159
- return refs;
160
- }
161
- function listenerFile(entry, from, app) {
162
- if (Node.isPropertyAccessExpression(entry)) {
163
- const root = entry.getExpression();
164
- if (!Node.isIdentifier(root)) return null;
165
- const registry = importedFrom(root.getText(), from, app);
166
- return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
167
- }
168
- if (Node.isIdentifier(entry)) {
169
- const target = importedFrom(entry.getText(), from, app);
170
- return target ? toPosix(target) : null;
171
- }
172
- return null;
173
- }
174
- /** where a local identifier was imported from, resolved through the alias map */
175
- function importedFrom(local, from, app) {
176
- for (const declaration of from.getImportDeclarations()) {
177
- const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
178
- const isDefault = declaration.getDefaultImport()?.getText() === local;
179
- if (!named && !isDefault) continue;
180
- return app.resolveSpecifier(declaration.getModuleSpecifierValue());
181
- }
182
- return null;
183
- }
184
- /**
185
- * The file a key of a generated registry points at.
186
- *
187
- * Both shapes the generators emit are handled: a direct reference to an
188
- * imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
189
- * per artefact and per framework version, and reading only one of them silently
190
- * lost half the graph.
191
- */
192
- function registryEntry(registryFile, key, project, app) {
193
- const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
194
- if (!file) return null;
195
- for (const declaration of file.getVariableDeclarations()) {
196
- const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
197
- if (!value) continue;
198
- if (Node.isIdentifier(value)) {
199
- const target = importedFrom(value.getText(), file, app);
200
- return target ? toPosix(target) : null;
201
- }
202
- const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
203
- const target = specifier ? app.resolveSpecifier(specifier) : null;
204
- return target ? toPosix(target) : null;
205
- }
206
- return null;
207
- }
208
- //#endregion
209
86
  //#region src/inventory/detectors/lucid.ts
210
87
  const WRITE_METHODS = new Set([
211
88
  "save",
@@ -512,6 +389,129 @@ function classOfReceiver(receiver) {
512
389
  return null;
513
390
  }
514
391
  //#endregion
392
+ //#region src/inventory/sources/event_bindings.ts
393
+ /** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
394
+ const LISTENER_METHOD = "handle";
395
+ function collectEventBindings(app) {
396
+ const project = new Project({
397
+ skipAddingFilesFromTsConfig: true,
398
+ skipFileDependencyResolution: true,
399
+ compilerOptions: { allowJs: false }
400
+ });
401
+ for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
402
+ const bindings = /* @__PURE__ */ new Map();
403
+ for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
404
+ const expression = call.getExpression();
405
+ if (!Node.isPropertyAccessExpression(expression)) continue;
406
+ if (expression.getName() !== "on") continue;
407
+ const [event, handlers] = call.getArguments();
408
+ if (!event || !handlers) continue;
409
+ const eventFile = resolveEventClass(event, file, app);
410
+ if (!eventFile) continue;
411
+ const refs = listenersOf(handlers, file, app);
412
+ if (refs.length === 0) continue;
413
+ bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
414
+ }
415
+ return bindings;
416
+ }
417
+ /**
418
+ * The event class a dispatch or a binding names.
419
+ *
420
+ * Two shapes reach here: the class imported directly, and the generated
421
+ * registry (`events.OrderPlaced`), which is what `node ace make:event` produces
422
+ * and therefore the common one. Exported because the resolver has to ask the
423
+ * same question of a call site, and two implementations of "which event is
424
+ * this" would drift.
425
+ */
426
+ function resolveEventClass(expression, from, app) {
427
+ if (Node.isIdentifier(expression)) {
428
+ const target = importedFrom(expression.getText(), from, app);
429
+ return target ? toPosix(target) : null;
430
+ }
431
+ if (!Node.isPropertyAccessExpression(expression)) return null;
432
+ const root = expression.getExpression();
433
+ if (!Node.isIdentifier(root)) return null;
434
+ const registry = importedFrom(root.getText(), from, app);
435
+ if (!registry) return null;
436
+ return registryEntry(registry, expression.getName(), from.getProject(), app);
437
+ }
438
+ /** listener bodies named by the second argument of `emitter.on` */
439
+ function listenersOf(handlers, from, app) {
440
+ const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
441
+ const refs = [];
442
+ for (const entry of entries) {
443
+ /**
444
+ * `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
445
+ * and taking `handle` on faith there would look for a body that is not
446
+ * the one bound.
447
+ */
448
+ if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
449
+ const [target, member] = entry.getElements();
450
+ const file = target ? listenerFile(target, from, app) : null;
451
+ if (!file) continue;
452
+ const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
453
+ refs.push({
454
+ file,
455
+ member: named ?? LISTENER_METHOD
456
+ });
457
+ continue;
458
+ }
459
+ const file = listenerFile(entry, from, app);
460
+ if (file) refs.push({
461
+ file,
462
+ member: LISTENER_METHOD
463
+ });
464
+ }
465
+ return refs;
466
+ }
467
+ function listenerFile(entry, from, app) {
468
+ if (Node.isPropertyAccessExpression(entry)) {
469
+ const root = entry.getExpression();
470
+ if (!Node.isIdentifier(root)) return null;
471
+ const registry = importedFrom(root.getText(), from, app);
472
+ return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
473
+ }
474
+ if (Node.isIdentifier(entry)) {
475
+ const target = importedFrom(entry.getText(), from, app);
476
+ return target ? toPosix(target) : null;
477
+ }
478
+ return null;
479
+ }
480
+ /** where a local identifier was imported from, resolved through the alias map */
481
+ function importedFrom(local, from, app) {
482
+ for (const declaration of from.getImportDeclarations()) {
483
+ const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
484
+ const isDefault = declaration.getDefaultImport()?.getText() === local;
485
+ if (!named && !isDefault) continue;
486
+ return app.resolveSpecifier(declaration.getModuleSpecifierValue());
487
+ }
488
+ return null;
489
+ }
490
+ /**
491
+ * The file a key of a generated registry points at.
492
+ *
493
+ * Both shapes the generators emit are handled: a direct reference to an
494
+ * imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
495
+ * per artefact and per framework version, and reading only one of them silently
496
+ * lost half the graph.
497
+ */
498
+ function registryEntry(registryFile, key, project, app) {
499
+ const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
500
+ if (!file) return null;
501
+ for (const declaration of file.getVariableDeclarations()) {
502
+ const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
503
+ if (!value) continue;
504
+ if (Node.isIdentifier(value)) {
505
+ const target = importedFrom(value.getText(), file, app);
506
+ return target ? toPosix(target) : null;
507
+ }
508
+ const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
509
+ const target = specifier ? app.resolveSpecifier(specifier) : null;
510
+ return target ? toPosix(target) : null;
511
+ }
512
+ return null;
513
+ }
514
+ //#endregion
515
515
  //#region src/inventory/resolvers/event_dispatch.ts
516
516
  /**
517
517
  * "Event" pattern: the handler announces, and listeners act.
@@ -619,6 +619,68 @@ const jobDispatchResolver = {
619
619
  }
620
620
  };
621
621
  //#endregion
622
+ //#region src/inventory/resolvers/local_function.ts
623
+ /**
624
+ * "Local function" pattern: a helper declared in the same file, not imported.
625
+ *
626
+ * const lista = await proximos(id) // function proximos() { … }
627
+ * return rows.map(paraLinha) // const paraLinha = (row) => …
628
+ *
629
+ * A query object that keeps its helpers beside it is common, and before this
630
+ * every such call was unresolved: the store a helper read was reached by nobody
631
+ * from that route, and a value built by it left as a whole table handed in.
632
+ *
633
+ * Only module-level declarations: a closure declared inside a body is part of
634
+ * that body, and the graph already walks it.
635
+ */
636
+ const localFunctionResolver = {
637
+ name: "local-function",
638
+ order: 45,
639
+ resolve(call, ctx) {
640
+ const named = calledFunctionOf(call);
641
+ if (!named) return [];
642
+ const name = named.getText();
643
+ if (ctx.imports.has(name)) return [];
644
+ if (!declaresFunction(ctx.file, name)) return [];
645
+ return [{
646
+ file: ctx.file.getFilePath(),
647
+ member: name
648
+ }];
649
+ }
650
+ };
651
+ /** callbacks that apply a function to each element: `rows.map(paraLinha)` calls `paraLinha` */
652
+ const APPLIES_CALLBACK = new Set([
653
+ "map",
654
+ "flatMap",
655
+ "forEach",
656
+ "filter",
657
+ "find",
658
+ "some",
659
+ "every",
660
+ "sort",
661
+ "reduce"
662
+ ]);
663
+ /**
664
+ * The function a call names: `proximos(id)` names `proximos`; `rows.map(paraLinha)`
665
+ * names `paraLinha`, called once per row — the body the graph must read is the
666
+ * same, whichever way it was reached.
667
+ */
668
+ function calledFunctionOf(call) {
669
+ const expression = call.getExpression();
670
+ if (Node.isIdentifier(expression)) return expression;
671
+ if (Node.isPropertyAccessExpression(expression) && APPLIES_CALLBACK.has(expression.getName())) {
672
+ const callback = call.getArguments()[0];
673
+ if (callback && Node.isIdentifier(callback)) return callback;
674
+ }
675
+ return null;
676
+ }
677
+ /** `function name() {}` or `const name = () => {}` / `function () {}` at module level */
678
+ function declaresFunction(file, name) {
679
+ if (file.getFunction(name)) return true;
680
+ const initializer = file.getVariableDeclaration(name)?.getInitializer();
681
+ return !!initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer));
682
+ }
683
+ //#endregion
622
684
  //#region src/inventory/resolvers/module_function.ts
623
685
  /**
624
686
  * "Module function" pattern: no class at all.
@@ -633,8 +695,8 @@ const moduleFunctionResolver = {
633
695
  name: "module-function",
634
696
  order: 50,
635
697
  resolve(call, ctx) {
636
- const expr = call.getExpression();
637
- if (!expr.isKind(SyntaxKind.Identifier)) return [];
698
+ const expr = calledFunctionOf(call);
699
+ if (!expr) return [];
638
700
  const local = expr.getText();
639
701
  const file = ctx.imports.get(local);
640
702
  if (!file) return [];
@@ -809,11 +871,37 @@ function outputFieldsIn(body, owner, stores, followed) {
809
871
  * data function applies (§6).
810
872
  */
811
873
  const excluded = new Set((resource ? stores.get(resource)?.attributes : void 0)?.filter((attribute) => attribute.isIdentifier || attribute.system).map((attribute) => attribute.name) ?? []);
874
+ const { leaves, opaque } = collectLeaves(returnedLiteralsOf(body), {
875
+ qualifier,
876
+ excluded,
877
+ followed
878
+ });
879
+ return {
880
+ outputs: leaves,
881
+ opaqueOutputs: opaque,
882
+ resource: resource && stores.has(resource) ? resource : null
883
+ };
884
+ }
885
+ /**
886
+ * The leaves of object literals, by the §7 rules:
887
+ *
888
+ * { titulo: l.titulo } 1 — `titulo`
889
+ * { autor: AutorTransformer.transform } 0 here; the followed body contributes
890
+ * { endereco: { rua, cidade } } leaves individually
891
+ * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
892
+ * { itens: xs.map((i) => ({ a, b })) } the leaves, once
893
+ * ...this.pick(this.resource, [...]) the listed names
894
+ * ...this.toObject() 0 here; the followed body contributes
895
+ * ...anythingElse 1, opaque, reported
896
+ */
897
+ function collectLeaves(literals, options) {
898
+ const { qualifier, excluded, followed } = options;
812
899
  const outputs = /* @__PURE__ */ new Set();
813
900
  const opaque = /* @__PURE__ */ new Set();
901
+ const q = qualifier ? `${qualifier}.` : "";
814
902
  const leaf = (prefix, name) => {
815
903
  if (prefix === "" && excluded.has(name)) return;
816
- outputs.add(`${qualifier}.${prefix ? `${prefix}.${name}` : name}`);
904
+ outputs.add(`${q}${prefix ? `${prefix}.${name}` : name}`);
817
905
  };
818
906
  const walk = (literal, prefix) => {
819
907
  for (const property of literal.getProperties()) {
@@ -830,7 +918,7 @@ function outputFieldsIn(body, owner, stores, followed) {
830
918
  continue;
831
919
  }
832
920
  if (!Node.isPropertyAssignment(property)) continue;
833
- const name = property.getName().replace(/^['"]|['"]$/g, "");
921
+ const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
834
922
  const value = unwrap(property.getInitializer());
835
923
  if (!value) {
836
924
  leaf(prefix, name);
@@ -874,15 +962,14 @@ function outputFieldsIn(body, owner, stores, followed) {
874
962
  * One DET as a floor, and reported — the placeholder carries the expression
875
963
  * so the report can name what could not be read.
876
964
  */
877
- const placeholder = `${qualifier}.${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
965
+ const placeholder = `${q}${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
878
966
  outputs.add(placeholder);
879
967
  opaque.add(placeholder);
880
968
  };
881
- for (const literal of returnedLiteralsOf(body)) walk(literal, "");
969
+ for (const literal of literals) walk(literal, "");
882
970
  return {
883
- outputs: [...outputs],
884
- opaqueOutputs: [...opaque],
885
- resource: resource && stores.has(resource) ? resource : null
971
+ leaves: [...outputs],
972
+ opaque: [...opaque]
886
973
  };
887
974
  }
888
975
  /**
@@ -1034,6 +1121,76 @@ const TRANSFORMER_METHODS = new Set([
1034
1121
  ]);
1035
1122
  /** what the application-side body is called */
1036
1123
  const APPLICATION_BODY = "toObject";
1124
+ /** the variant named in these is a method of the transformer, and part of what leaves */
1125
+ const VARIANT_METHODS = new Set(["useVariant", "withVariant"]);
1126
+ /**
1127
+ * "Transformer" pattern: the package supplies the API, the application
1128
+ * supplies the body.
1129
+ *
1130
+ * class InviteTransformer extends BaseTransformer<Invite> {
1131
+ * toObject() { … }
1132
+ * }
1133
+ *
1134
+ * InviteTransformer.transform(invite)
1135
+ *
1136
+ * `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
1137
+ * symbol lands on the application file and finds no body there. The naive
1138
+ * reading is that the tracer must step into node_modules; it does not. Those
1139
+ * methods call BACK into `toObject()`, which the application writes, so the
1140
+ * body worth analysing was in the application all along.
1141
+ *
1142
+ * It is the same shape as `job-dispatch`, where `dispatch` enqueues and
1143
+ * `handle` executes.
1144
+ *
1145
+ * COUNTING DECISION: the write a transformer performs belongs to the
1146
+ * transaction that serialised through it. Without this, a table written only
1147
+ * inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
1148
+ */
1149
+ /** is `.useVariant('x')` / `.withVariant('x')` the next link of this call's chain? */
1150
+ function variantFollows(call) {
1151
+ let node = call;
1152
+ for (let depth = 0; depth < 20; depth++) {
1153
+ const parent = node.getParent();
1154
+ if (!parent) return false;
1155
+ if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
1156
+ node = parent;
1157
+ continue;
1158
+ }
1159
+ if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) return false;
1160
+ const next = parent.getParent();
1161
+ if (!next || !Node.isCallExpression(next) || next.getExpression() !== parent) return false;
1162
+ if (VARIANT_METHODS.has(parent.getName())) return next.getArguments()[0]?.getKind() === SyntaxKind.StringLiteral;
1163
+ node = next;
1164
+ }
1165
+ return false;
1166
+ }
1167
+ /** the transformer class a chain call is on, if it is one: `X.transform(p)`, `X.transform(p).useVariant(v)` */
1168
+ function transformerOf(call, ctx) {
1169
+ const expression = call.getExpression();
1170
+ if (!Node.isPropertyAccessExpression(expression)) return null;
1171
+ if (!TRANSFORMER_METHODS.has(expression.getName())) return null;
1172
+ /**
1173
+ * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
1174
+ * well as `X.transform(p)`. Analysing the same body twice is free: the
1175
+ * graph dedupes by file and member.
1176
+ */
1177
+ const symbol = rootSymbolOf(expression.getExpression());
1178
+ if (!symbol) return null;
1179
+ const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
1180
+ if (!file) return null;
1181
+ /**
1182
+ * One definition of what a transformer is, shared with the output walker
1183
+ * (`graph/output_fields.ts`): the resolver must not follow a body whose keys
1184
+ * the walker would refuse to read, or the other way round.
1185
+ */
1186
+ const declared = ctx.sourceFile(file);
1187
+ if (!declared || !declared.getClasses().some(isTransformerClass)) return null;
1188
+ const owner = declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY));
1189
+ return owner ? {
1190
+ file,
1191
+ owner
1192
+ } : null;
1193
+ }
1037
1194
  //#endregion
1038
1195
  //#region src/inventory/resolvers/index.ts
1039
1196
  /**
@@ -1051,34 +1208,50 @@ const BUILTIN_CALL_RESOLVERS = [
1051
1208
  {
1052
1209
  name: "transformer",
1053
1210
  order: 18,
1211
+ /**
1212
+ * The `transform` before a `useVariant`: claimed and followed nowhere, or
1213
+ * `static-service` would take it next, look for a `transform` body in the
1214
+ * application file and report the package method as a gap.
1215
+ */
1216
+ ignores(call, ctx) {
1217
+ const expression = call.getExpression();
1218
+ if (!Node.isPropertyAccessExpression(expression)) return false;
1219
+ if (VARIANT_METHODS.has(expression.getName()) || !variantFollows(call)) return false;
1220
+ return transformerOf(call, ctx) !== null;
1221
+ },
1054
1222
  resolve(call, ctx) {
1055
1223
  const expression = call.getExpression();
1056
1224
  if (!Node.isPropertyAccessExpression(expression)) return [];
1057
- if (!TRANSFORMER_METHODS.has(expression.getName())) return [];
1058
- /**
1059
- * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
1060
- * well as `X.transform(p)`. Analysing the same body twice is free: the
1061
- * graph dedupes by file and member.
1062
- */
1063
- const symbol = rootSymbolOf(expression.getExpression());
1064
- if (!symbol) return [];
1065
- const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
1066
- if (!file) return [];
1225
+ const found = transformerOf(call, ctx);
1226
+ if (!found) return [];
1227
+ const { file, owner } = found;
1067
1228
  /**
1068
- * One definition of what a transformer is, shared with the output walker
1069
- * (`graph/output_fields.ts`): the resolver must not follow a body whose keys
1070
- * the walker would refuse to read, or the other way round.
1229
+ * `X.transform(p).useVariant('forEgresso')`: the variant is a METHOD of the
1230
+ * transformer, named after it, and it REPLACES `toObject()` — the shape that
1231
+ * leaves is the variant's. The chain is visited call by call: the `useVariant`
1232
+ * call resolves the variant's body, and the `transform` call before it resolves
1233
+ * nothing, or both shapes would leave and the page would count twice.
1071
1234
  */
1072
- const declared = ctx.sourceFile(file);
1073
- if (!declared || !declared.getClasses().some(isTransformerClass)) return [];
1074
- return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
1235
+ if (VARIANT_METHODS.has(expression.getName())) {
1236
+ const variant = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
1237
+ return variant && owner.getMethod(variant) ? [{
1238
+ file,
1239
+ member: variant
1240
+ }] : [{
1241
+ file,
1242
+ member: APPLICATION_BODY
1243
+ }];
1244
+ }
1245
+ if (variantFollows(call)) return [];
1246
+ return [{
1075
1247
  file,
1076
1248
  member: APPLICATION_BODY
1077
- }] : [];
1249
+ }];
1078
1250
  }
1079
1251
  },
1080
1252
  staticServiceResolver,
1081
1253
  propertyServiceResolver,
1254
+ localFunctionResolver,
1082
1255
  moduleFunctionResolver
1083
1256
  ];
1084
1257
  /**
@@ -1147,4 +1320,4 @@ function ignoreCalls(options) {
1147
1320
  };
1148
1321
  }
1149
1322
  //#endregion
1150
- export { chainShapeOf as a, hooksFiredBy as c, isApplicationCode as d, isSeeder as f, toPosix as h, resolveCall as i, rootSymbolOf as l, samePath as m, ignoreCalls as n, outputFieldsIn as o, relativeTo as p, isTechnicalWrite as r, detectAccess as s, BUILTIN_CALL_RESOLVERS as t, collectEventBindings as u };
1323
+ export { relativeTo as _, chainShapeOf as a, unwrap as c, collectEventBindings as d, detectAccess as f, isSeeder as g, isApplicationCode as h, resolveCall as i, DISPATCH_METHODS as l, rootSymbolOf as m, ignoreCalls as n, mappedLiteralOf as o, hooksFiredBy as p, isTechnicalWrite as r, outputFieldsIn as s, BUILTIN_CALL_RESOLVERS as t, EXECUTION_METHODS as u, samePath as v, toPosix as y };
@@ -1,6 +1,6 @@
1
1
  import { c as IncomparableSourcesError, d as DEFAULTS, f as defineConfig, i as measureStructure, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
2
- import { h as toPosix } from "./resolvers-DlKJOZnk.js";
3
- import { n as analyze } from "./pipeline-Cq4dNTNE.js";
2
+ import { y as toPosix } from "./resolvers-DaU4uAqT.js";
3
+ import { n as analyze } from "./pipeline-DO2301fV.js";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { existsSync } from "node:fs";
@@ -3,6 +3,7 @@ import type { CollectedDataStore } from '../inventory/sources/data_stores.js';
3
3
  import type { CollectedEntryPoint } from '../inventory/sources/routes_ast.js';
4
4
  import type { Behavior } from '../inventory/graph/call_graph.js';
5
5
  import type { DiscoveredSchema } from '../inventory/sources/json_schemas.js';
6
+ import type { CollectedJob } from '../inventory/sources/jobs.js';
6
7
  import type { OpaqueDeclaration } from './opaque.js';
7
8
  import type { Complexity, CountResult, FunctionType } from '../types.js';
8
9
  import type { FunctionOverride } from '../define_config.js';
@@ -36,12 +37,16 @@ export declare const RULESET = "afp";
36
37
  * column; system timestamps and `serializeAs: null` columns leaving the DETs;
37
38
  * master-detail folded into one data function; identity by table; token tables
38
39
  * technical; and opaque declarations reaching every function carrying the origin.
40
+ * Three in 1.6.0: an output's DETs are what the transaction DELIVERS (the render
41
+ * props, the response payload, what a command prints) read back to their origin;
42
+ * a function of the same file and a `.map(fn)` by reference are followed, so
43
+ * FTRs move; and ace commands are transactions, with flags as input.
39
44
  *
40
45
  * Without the bump, a baseline saved by the previous version compares cleanly
41
46
  * against this one and bills the tool's own improvement as work done. The guard
42
47
  * exists for exactly that, and only this constant arms it.
43
48
  */
44
- export declare const RULESET_VERSION = "1.5.0";
49
+ export declare const RULESET_VERSION = "1.6.0";
45
50
  export type CountInput = {
46
51
  app: AppContext;
47
52
  stores: CollectedDataStore[];
@@ -50,6 +55,8 @@ export type CountInput = {
50
55
  behaviors: Map<string, Behavior>;
51
56
  /** JSON Schema literals found in the code, for `detFromSchema` — §8 */
52
57
  jsonSchemas?: Map<string, DiscoveredSchema>;
58
+ /** the queue jobs and who dispatches each — a job no transaction reaches is reported (plan 0.7 §D) */
59
+ jobs?: CollectedJob[];
53
60
  /**
54
61
  * Stores written anywhere in the application's code, reachable from an entry
55
62
  * point or not — AFP §6.5.4 asks who MAINTAINS the store, and a job or a
package/build/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as CoverageTooLowError } from "../pipeline-Cq4dNTNE.js";
2
- import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-FYmPIPub.js";
1
+ import { t as CoverageTooLowError } from "../pipeline-DO2301fV.js";
2
+ import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-Dm7cWGa-.js";
3
3
  import path from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -53,6 +53,8 @@ export type Behavior = {
53
53
  * not a validator.
54
54
  */
55
55
  requestFields: string[];
56
+ /** an ace command's `@flags.*` / `@args.*`: `flags.limite`, `args.name` — its input DETs */
57
+ commandFields: string[];
56
58
  /** the transaction reads the request in a way that enumerates nothing */
57
59
  opaqueRequest: boolean;
58
60
  /**
@@ -82,6 +84,19 @@ export type Behavior = {
82
84
  /** stores it was preloaded through */
83
85
  via: string[];
84
86
  }>;
87
+ /**
88
+ * What the transaction DELIVERS — plan 0.7 §A′. `any` says a delivery point was
89
+ * found at all; without one the output falls back to the stores read. `fields`
90
+ * are derived values and the leaves of literals a followed body returned,
91
+ * `render:total`; `stores` are the ones whose rows were handed on, raw or through
92
+ * a query object; `opaqueFields` are values nobody could read, 1 DET each.
93
+ */
94
+ delivered: {
95
+ any: boolean;
96
+ fields: string[];
97
+ opaqueFields: string[];
98
+ stores: string[];
99
+ };
85
100
  trace: TraceStep[];
86
101
  /** bodies reached, for `fp:diff` */
87
102
  scope: ScopeEntry[];