@filipebraida/adonis-function-points 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +1 -1
  3. package/build/commands/main.js +6 -6
  4. package/build/{fp_calibrate-EAuAtdbq.js → fp_calibrate-B_oD9b02.js} +1 -1
  5. package/build/{fp_count-CZ0cUUBQ.js → fp_count-B_KPNKMO.js} +1 -1
  6. package/build/{fp_diff-BTg_LX0r.js → fp_diff-B3NhXzrb.js} +1 -1
  7. package/build/{fp_explain-D6QvDLKQ.js → fp_explain-C2s6Kpaj.js} +1 -1
  8. package/build/{fp_inventory-C43fU39x.js → fp_inventory-B322MvZT.js} +1 -1
  9. package/build/{fp_metrics-DEMPk4xC.js → fp_metrics-DjADb7F6.js} +1 -1
  10. package/build/index.js +2 -2
  11. package/build/{pipeline-Cq4dNTNE.js → pipeline-C6kHKB9-.js} +2363 -114
  12. package/build/{resolvers-DlKJOZnk.js → resolvers-DhJO-qvQ.js} +339 -151
  13. package/build/{runners-FYmPIPub.js → runners-BpBJsGMj.js} +2 -2
  14. package/build/src/albrecht/counter.d.ts +12 -1
  15. package/build/src/cli.js +2 -2
  16. package/build/src/inventory/graph/call_graph.d.ts +22 -0
  17. package/build/src/inventory/graph/deliveries.d.ts +94 -0
  18. package/build/src/inventory/graph/output_fields.d.ts +45 -1
  19. package/build/src/inventory/graph/pages.d.ts +44 -0
  20. package/build/src/inventory/resolvers/index.js +1 -1
  21. package/build/src/inventory/resolvers/job_dispatch.d.ts +20 -0
  22. package/build/src/inventory/resolvers/local_function.d.ts +24 -0
  23. package/build/src/inventory/resolvers/transformer.d.ts +0 -23
  24. package/build/src/inventory/sources/commands.d.ts +14 -0
  25. package/build/src/inventory/sources/jobs.d.ts +27 -0
  26. package/build/src/pipeline.js +1 -1
  27. package/build/src/types.d.ts +23 -0
  28. package/package.json +1 -1
@@ -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",
@@ -503,11 +380,149 @@ function classOfReceiver(receiver) {
503
380
  return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
504
381
  }
505
382
  if (receiver.isKind(SyntaxKind.Identifier)) {
506
- const init = (receiver.getSymbol()?.getDeclarations().find((d) => d.isKind(SyntaxKind.VariableDeclaration)))?.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
383
+ let init = (receiver.getSymbol()?.getDeclarations().find((d) => d.isKind(SyntaxKind.VariableDeclaration)))?.asKind(SyntaxKind.VariableDeclaration)?.getInitializer();
384
+ while (init?.isKind(SyntaxKind.AwaitExpression) || init?.isKind(SyntaxKind.ParenthesizedExpression)) init = init.getExpression();
507
385
  if (init?.isKind(SyntaxKind.NewExpression)) {
508
386
  const target = init.getExpression();
509
387
  return target.isKind(SyntaxKind.Identifier) ? target.getText() : null;
510
388
  }
389
+ /**
390
+ * `const svc = await app.container.make(IntakeService)`: the container hands
391
+ * back an instance of the class named — the same binding as `new`, written the
392
+ * way a controller writes it when the service has dependencies of its own. On a
393
+ * reviewed application this shape carried the write of `POST /gestao/atribuicao`
394
+ * and 31 more sites, and none was followed (plan 0.8 §B).
395
+ */
396
+ if (init?.isKind(SyntaxKind.CallExpression)) {
397
+ const callee = init.getExpression();
398
+ if (callee.isKind(SyntaxKind.PropertyAccessExpression) && callee.getName() === "make" && callee.getExpression().isKind(SyntaxKind.PropertyAccessExpression) && callee.getExpression().asKind(SyntaxKind.PropertyAccessExpression).getName() === "container") {
399
+ const made = init.getArguments()[0];
400
+ return made?.isKind(SyntaxKind.Identifier) ? made.getText() : null;
401
+ }
402
+ }
403
+ }
404
+ return null;
405
+ }
406
+ //#endregion
407
+ //#region src/inventory/sources/event_bindings.ts
408
+ /** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
409
+ const LISTENER_METHOD = "handle";
410
+ function collectEventBindings(app) {
411
+ const project = new Project({
412
+ skipAddingFilesFromTsConfig: true,
413
+ skipFileDependencyResolution: true,
414
+ compilerOptions: { allowJs: false }
415
+ });
416
+ for (const root of app.scanRoots) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
417
+ const bindings = /* @__PURE__ */ new Map();
418
+ for (const file of project.getSourceFiles()) for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
419
+ const expression = call.getExpression();
420
+ if (!Node.isPropertyAccessExpression(expression)) continue;
421
+ if (expression.getName() !== "on") continue;
422
+ const [event, handlers] = call.getArguments();
423
+ if (!event || !handlers) continue;
424
+ const eventFile = resolveEventClass(event, file, app);
425
+ if (!eventFile) continue;
426
+ const refs = listenersOf(handlers, file, app);
427
+ if (refs.length === 0) continue;
428
+ bindings.set(eventFile, [...bindings.get(eventFile) ?? [], ...refs]);
429
+ }
430
+ return bindings;
431
+ }
432
+ /**
433
+ * The event class a dispatch or a binding names.
434
+ *
435
+ * Two shapes reach here: the class imported directly, and the generated
436
+ * registry (`events.OrderPlaced`), which is what `node ace make:event` produces
437
+ * and therefore the common one. Exported because the resolver has to ask the
438
+ * same question of a call site, and two implementations of "which event is
439
+ * this" would drift.
440
+ */
441
+ function resolveEventClass(expression, from, app) {
442
+ if (Node.isIdentifier(expression)) {
443
+ const target = importedFrom(expression.getText(), from, app);
444
+ return target ? toPosix(target) : null;
445
+ }
446
+ if (!Node.isPropertyAccessExpression(expression)) return null;
447
+ const root = expression.getExpression();
448
+ if (!Node.isIdentifier(root)) return null;
449
+ const registry = importedFrom(root.getText(), from, app);
450
+ if (!registry) return null;
451
+ return registryEntry(registry, expression.getName(), from.getProject(), app);
452
+ }
453
+ /** listener bodies named by the second argument of `emitter.on` */
454
+ function listenersOf(handlers, from, app) {
455
+ const entries = handlers.isKind(SyntaxKind.ArrayLiteralExpression) ? handlers.getElements() : [handlers];
456
+ const refs = [];
457
+ for (const entry of entries) {
458
+ /**
459
+ * `[SomeListener, 'method']`: AdonisJS lets the binding name the method,
460
+ * and taking `handle` on faith there would look for a body that is not
461
+ * the one bound.
462
+ */
463
+ if (entry.isKind(SyntaxKind.ArrayLiteralExpression)) {
464
+ const [target, member] = entry.getElements();
465
+ const file = target ? listenerFile(target, from, app) : null;
466
+ if (!file) continue;
467
+ const named = member?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
468
+ refs.push({
469
+ file,
470
+ member: named ?? LISTENER_METHOD
471
+ });
472
+ continue;
473
+ }
474
+ const file = listenerFile(entry, from, app);
475
+ if (file) refs.push({
476
+ file,
477
+ member: LISTENER_METHOD
478
+ });
479
+ }
480
+ return refs;
481
+ }
482
+ function listenerFile(entry, from, app) {
483
+ if (Node.isPropertyAccessExpression(entry)) {
484
+ const root = entry.getExpression();
485
+ if (!Node.isIdentifier(root)) return null;
486
+ const registry = importedFrom(root.getText(), from, app);
487
+ return registry ? registryEntry(registry, entry.getName(), from.getProject(), app) : null;
488
+ }
489
+ if (Node.isIdentifier(entry)) {
490
+ const target = importedFrom(entry.getText(), from, app);
491
+ return target ? toPosix(target) : null;
492
+ }
493
+ return null;
494
+ }
495
+ /** where a local identifier was imported from, resolved through the alias map */
496
+ function importedFrom(local, from, app) {
497
+ for (const declaration of from.getImportDeclarations()) {
498
+ const named = declaration.getNamedImports().some((entry) => (entry.getAliasNode()?.getText() ?? entry.getName()) === local);
499
+ const isDefault = declaration.getDefaultImport()?.getText() === local;
500
+ if (!named && !isDefault) continue;
501
+ return app.resolveSpecifier(declaration.getModuleSpecifierValue());
502
+ }
503
+ return null;
504
+ }
505
+ /**
506
+ * The file a key of a generated registry points at.
507
+ *
508
+ * Both shapes the generators emit are handled: a direct reference to an
509
+ * imported class (`events.ts`) and a lazy importer (`listeners.ts`). They differ
510
+ * per artefact and per framework version, and reading only one of them silently
511
+ * lost half the graph.
512
+ */
513
+ function registryEntry(registryFile, key, project, app) {
514
+ const file = project.getSourceFile(registryFile) ?? project.addSourceFileAtPathIfExists(registryFile);
515
+ if (!file) return null;
516
+ for (const declaration of file.getVariableDeclarations()) {
517
+ const value = ((declaration.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression))?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment))?.getInitializer();
518
+ if (!value) continue;
519
+ if (Node.isIdentifier(value)) {
520
+ const target = importedFrom(value.getText(), file, app);
521
+ return target ? toPosix(target) : null;
522
+ }
523
+ const specifier = value.getFirstDescendantByKind(SyntaxKind.CallExpression)?.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
524
+ const target = specifier ? app.resolveSpecifier(specifier) : null;
525
+ return target ? toPosix(target) : null;
511
526
  }
512
527
  return null;
513
528
  }
@@ -619,6 +634,68 @@ const jobDispatchResolver = {
619
634
  }
620
635
  };
621
636
  //#endregion
637
+ //#region src/inventory/resolvers/local_function.ts
638
+ /**
639
+ * "Local function" pattern: a helper declared in the same file, not imported.
640
+ *
641
+ * const lista = await proximos(id) // function proximos() { … }
642
+ * return rows.map(paraLinha) // const paraLinha = (row) => …
643
+ *
644
+ * A query object that keeps its helpers beside it is common, and before this
645
+ * every such call was unresolved: the store a helper read was reached by nobody
646
+ * from that route, and a value built by it left as a whole table handed in.
647
+ *
648
+ * Only module-level declarations: a closure declared inside a body is part of
649
+ * that body, and the graph already walks it.
650
+ */
651
+ const localFunctionResolver = {
652
+ name: "local-function",
653
+ order: 45,
654
+ resolve(call, ctx) {
655
+ const named = calledFunctionOf(call);
656
+ if (!named) return [];
657
+ const name = named.getText();
658
+ if (ctx.imports.has(name)) return [];
659
+ if (!declaresFunction(ctx.file, name)) return [];
660
+ return [{
661
+ file: ctx.file.getFilePath(),
662
+ member: name
663
+ }];
664
+ }
665
+ };
666
+ /** callbacks that apply a function to each element: `rows.map(paraLinha)` calls `paraLinha` */
667
+ const APPLIES_CALLBACK = new Set([
668
+ "map",
669
+ "flatMap",
670
+ "forEach",
671
+ "filter",
672
+ "find",
673
+ "some",
674
+ "every",
675
+ "sort",
676
+ "reduce"
677
+ ]);
678
+ /**
679
+ * The function a call names: `proximos(id)` names `proximos`; `rows.map(paraLinha)`
680
+ * names `paraLinha`, called once per row — the body the graph must read is the
681
+ * same, whichever way it was reached.
682
+ */
683
+ function calledFunctionOf(call) {
684
+ const expression = call.getExpression();
685
+ if (Node.isIdentifier(expression)) return expression;
686
+ if (Node.isPropertyAccessExpression(expression) && APPLIES_CALLBACK.has(expression.getName())) {
687
+ const callback = call.getArguments()[0];
688
+ if (callback && Node.isIdentifier(callback)) return callback;
689
+ }
690
+ return null;
691
+ }
692
+ /** `function name() {}` or `const name = () => {}` / `function () {}` at module level */
693
+ function declaresFunction(file, name) {
694
+ if (file.getFunction(name)) return true;
695
+ const initializer = file.getVariableDeclaration(name)?.getInitializer();
696
+ return !!initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer));
697
+ }
698
+ //#endregion
622
699
  //#region src/inventory/resolvers/module_function.ts
623
700
  /**
624
701
  * "Module function" pattern: no class at all.
@@ -633,8 +710,8 @@ const moduleFunctionResolver = {
633
710
  name: "module-function",
634
711
  order: 50,
635
712
  resolve(call, ctx) {
636
- const expr = call.getExpression();
637
- if (!expr.isKind(SyntaxKind.Identifier)) return [];
713
+ const expr = calledFunctionOf(call);
714
+ if (!expr) return [];
638
715
  const local = expr.getText();
639
716
  const file = ctx.imports.get(local);
640
717
  if (!file) return [];
@@ -809,11 +886,37 @@ function outputFieldsIn(body, owner, stores, followed) {
809
886
  * data function applies (§6).
810
887
  */
811
888
  const excluded = new Set((resource ? stores.get(resource)?.attributes : void 0)?.filter((attribute) => attribute.isIdentifier || attribute.system).map((attribute) => attribute.name) ?? []);
889
+ const { leaves, opaque } = collectLeaves(returnedLiteralsOf(body), {
890
+ qualifier,
891
+ excluded,
892
+ followed
893
+ });
894
+ return {
895
+ outputs: leaves,
896
+ opaqueOutputs: opaque,
897
+ resource: resource && stores.has(resource) ? resource : null
898
+ };
899
+ }
900
+ /**
901
+ * The leaves of object literals, by the §7 rules:
902
+ *
903
+ * { titulo: l.titulo } 1 — `titulo`
904
+ * { autor: AutorTransformer.transform } 0 here; the followed body contributes
905
+ * { endereco: { rua, cidade } } leaves individually
906
+ * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
907
+ * { itens: xs.map((i) => ({ a, b })) } the leaves, once
908
+ * ...this.pick(this.resource, [...]) the listed names
909
+ * ...this.toObject() 0 here; the followed body contributes
910
+ * ...anythingElse 1, opaque, reported
911
+ */
912
+ function collectLeaves(literals, options) {
913
+ const { qualifier, excluded, followed } = options;
812
914
  const outputs = /* @__PURE__ */ new Set();
813
915
  const opaque = /* @__PURE__ */ new Set();
916
+ const q = qualifier ? `${qualifier}.` : "";
814
917
  const leaf = (prefix, name) => {
815
918
  if (prefix === "" && excluded.has(name)) return;
816
- outputs.add(`${qualifier}.${prefix ? `${prefix}.${name}` : name}`);
919
+ outputs.add(`${q}${prefix ? `${prefix}.${name}` : name}`);
817
920
  };
818
921
  const walk = (literal, prefix) => {
819
922
  for (const property of literal.getProperties()) {
@@ -830,7 +933,7 @@ function outputFieldsIn(body, owner, stores, followed) {
830
933
  continue;
831
934
  }
832
935
  if (!Node.isPropertyAssignment(property)) continue;
833
- const name = property.getName().replace(/^['"]|['"]$/g, "");
936
+ const name = Node.isComputedPropertyName(property.getNameNode()) ? "*" : property.getName().replace(/^['"]|['"]$/g, "");
834
937
  const value = unwrap(property.getInitializer());
835
938
  if (!value) {
836
939
  leaf(prefix, name);
@@ -874,15 +977,14 @@ function outputFieldsIn(body, owner, stores, followed) {
874
977
  * One DET as a floor, and reported — the placeholder carries the expression
875
978
  * so the report can name what could not be read.
876
979
  */
877
- const placeholder = `${qualifier}.${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
980
+ const placeholder = `${q}${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
878
981
  outputs.add(placeholder);
879
982
  opaque.add(placeholder);
880
983
  };
881
- for (const literal of returnedLiteralsOf(body)) walk(literal, "");
984
+ for (const literal of literals) walk(literal, "");
882
985
  return {
883
- outputs: [...outputs],
884
- opaqueOutputs: [...opaque],
885
- resource: resource && stores.has(resource) ? resource : null
986
+ leaves: [...outputs],
987
+ opaque: [...opaque]
886
988
  };
887
989
  }
888
990
  /**
@@ -1034,6 +1136,76 @@ const TRANSFORMER_METHODS = new Set([
1034
1136
  ]);
1035
1137
  /** what the application-side body is called */
1036
1138
  const APPLICATION_BODY = "toObject";
1139
+ /** the variant named in these is a method of the transformer, and part of what leaves */
1140
+ const VARIANT_METHODS = new Set(["useVariant", "withVariant"]);
1141
+ /**
1142
+ * "Transformer" pattern: the package supplies the API, the application
1143
+ * supplies the body.
1144
+ *
1145
+ * class InviteTransformer extends BaseTransformer<Invite> {
1146
+ * toObject() { … }
1147
+ * }
1148
+ *
1149
+ * InviteTransformer.transform(invite)
1150
+ *
1151
+ * `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
1152
+ * symbol lands on the application file and finds no body there. The naive
1153
+ * reading is that the tracer must step into node_modules; it does not. Those
1154
+ * methods call BACK into `toObject()`, which the application writes, so the
1155
+ * body worth analysing was in the application all along.
1156
+ *
1157
+ * It is the same shape as `job-dispatch`, where `dispatch` enqueues and
1158
+ * `handle` executes.
1159
+ *
1160
+ * COUNTING DECISION: the write a transformer performs belongs to the
1161
+ * transaction that serialised through it. Without this, a table written only
1162
+ * inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
1163
+ */
1164
+ /** is `.useVariant('x')` / `.withVariant('x')` the next link of this call's chain? */
1165
+ function variantFollows(call) {
1166
+ let node = call;
1167
+ for (let depth = 0; depth < 20; depth++) {
1168
+ const parent = node.getParent();
1169
+ if (!parent) return false;
1170
+ if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
1171
+ node = parent;
1172
+ continue;
1173
+ }
1174
+ if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) return false;
1175
+ const next = parent.getParent();
1176
+ if (!next || !Node.isCallExpression(next) || next.getExpression() !== parent) return false;
1177
+ if (VARIANT_METHODS.has(parent.getName())) return next.getArguments()[0]?.getKind() === SyntaxKind.StringLiteral;
1178
+ node = next;
1179
+ }
1180
+ return false;
1181
+ }
1182
+ /** the transformer class a chain call is on, if it is one: `X.transform(p)`, `X.transform(p).useVariant(v)` */
1183
+ function transformerOf(call, ctx) {
1184
+ const expression = call.getExpression();
1185
+ if (!Node.isPropertyAccessExpression(expression)) return null;
1186
+ if (!TRANSFORMER_METHODS.has(expression.getName())) return null;
1187
+ /**
1188
+ * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
1189
+ * well as `X.transform(p)`. Analysing the same body twice is free: the
1190
+ * graph dedupes by file and member.
1191
+ */
1192
+ const symbol = rootSymbolOf(expression.getExpression());
1193
+ if (!symbol) return null;
1194
+ const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
1195
+ if (!file) return null;
1196
+ /**
1197
+ * One definition of what a transformer is, shared with the output walker
1198
+ * (`graph/output_fields.ts`): the resolver must not follow a body whose keys
1199
+ * the walker would refuse to read, or the other way round.
1200
+ */
1201
+ const declared = ctx.sourceFile(file);
1202
+ if (!declared || !declared.getClasses().some(isTransformerClass)) return null;
1203
+ const owner = declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY));
1204
+ return owner ? {
1205
+ file,
1206
+ owner
1207
+ } : null;
1208
+ }
1037
1209
  //#endregion
1038
1210
  //#region src/inventory/resolvers/index.ts
1039
1211
  /**
@@ -1051,34 +1223,50 @@ const BUILTIN_CALL_RESOLVERS = [
1051
1223
  {
1052
1224
  name: "transformer",
1053
1225
  order: 18,
1226
+ /**
1227
+ * The `transform` before a `useVariant`: claimed and followed nowhere, or
1228
+ * `static-service` would take it next, look for a `transform` body in the
1229
+ * application file and report the package method as a gap.
1230
+ */
1231
+ ignores(call, ctx) {
1232
+ const expression = call.getExpression();
1233
+ if (!Node.isPropertyAccessExpression(expression)) return false;
1234
+ if (VARIANT_METHODS.has(expression.getName()) || !variantFollows(call)) return false;
1235
+ return transformerOf(call, ctx) !== null;
1236
+ },
1054
1237
  resolve(call, ctx) {
1055
1238
  const expression = call.getExpression();
1056
1239
  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 [];
1240
+ const found = transformerOf(call, ctx);
1241
+ if (!found) return [];
1242
+ const { file, owner } = found;
1067
1243
  /**
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.
1244
+ * `X.transform(p).useVariant('forEgresso')`: the variant is a METHOD of the
1245
+ * transformer, named after it, and it REPLACES `toObject()` — the shape that
1246
+ * leaves is the variant's. The chain is visited call by call: the `useVariant`
1247
+ * call resolves the variant's body, and the `transform` call before it resolves
1248
+ * nothing, or both shapes would leave and the page would count twice.
1071
1249
  */
1072
- const declared = ctx.sourceFile(file);
1073
- if (!declared || !declared.getClasses().some(isTransformerClass)) return [];
1074
- return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
1250
+ if (VARIANT_METHODS.has(expression.getName())) {
1251
+ const variant = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
1252
+ return variant && owner.getMethod(variant) ? [{
1253
+ file,
1254
+ member: variant
1255
+ }] : [{
1256
+ file,
1257
+ member: APPLICATION_BODY
1258
+ }];
1259
+ }
1260
+ if (variantFollows(call)) return [];
1261
+ return [{
1075
1262
  file,
1076
1263
  member: APPLICATION_BODY
1077
- }] : [];
1264
+ }];
1078
1265
  }
1079
1266
  },
1080
1267
  staticServiceResolver,
1081
1268
  propertyServiceResolver,
1269
+ localFunctionResolver,
1082
1270
  moduleFunctionResolver
1083
1271
  ];
1084
1272
  /**
@@ -1147,4 +1335,4 @@ function ignoreCalls(options) {
1147
1335
  };
1148
1336
  }
1149
1337
  //#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 };
1338
+ 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-DhJO-qvQ.js";
3
+ import { n as analyze } from "./pipeline-C6kHKB9-.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,20 @@ 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. One in
44
+ * 1.7.0, wide: a write binds to what the variable IS — a destructured named
45
+ * interface, a followed method's return, a relation off a row, a loop over rows,
46
+ * the guard's user, a service the container made — and a write nobody can type is
47
+ * an unresolved call instead of silence.
39
48
  *
40
49
  * Without the bump, a baseline saved by the previous version compares cleanly
41
50
  * against this one and bills the tool's own improvement as work done. The guard
42
51
  * exists for exactly that, and only this constant arms it.
43
52
  */
44
- export declare const RULESET_VERSION = "1.5.0";
53
+ export declare const RULESET_VERSION = "1.7.0";
45
54
  export type CountInput = {
46
55
  app: AppContext;
47
56
  stores: CollectedDataStore[];
@@ -50,6 +59,8 @@ export type CountInput = {
50
59
  behaviors: Map<string, Behavior>;
51
60
  /** JSON Schema literals found in the code, for `detFromSchema` — §8 */
52
61
  jsonSchemas?: Map<string, DiscoveredSchema>;
62
+ /** the queue jobs and who dispatches each — a job no transaction reaches is reported (plan 0.7 §D) */
63
+ jobs?: CollectedJob[];
53
64
  /**
54
65
  * Stores written anywhere in the application's code, reachable from an entry
55
66
  * 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-C6kHKB9-.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-BpBJsGMj.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,26 @@ 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
+ };
100
+ /**
101
+ * Of the stores delivered raw to a page, the columns the page reads off them
102
+ * (plan 0.8 §D). A store absent here leaves whole; one in `unreadablePages` leaves
103
+ * whole because the page could not be read, and says why.
104
+ */
105
+ pageReads: Record<string, string[]>;
106
+ unreadablePages: Record<string, string>;
85
107
  trace: TraceStep[];
86
108
  /** bodies reached, for `fp:diff` */
87
109
  scope: ScopeEntry[];