@filipebraida/adonis-function-points 0.4.0 → 0.6.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 (34) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +101 -281
  3. package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
  4. package/build/commands/main.js +6 -6
  5. package/build/{fp_calibrate-DLZP5bUp.js → fp_calibrate-EAuAtdbq.js} +1 -1
  6. package/build/{fp_count-DNSwaLUD.js → fp_count-CZ0cUUBQ.js} +1 -1
  7. package/build/{fp_diff-CCKxqGKh.js → fp_diff-BTg_LX0r.js} +1 -1
  8. package/build/{fp_explain-Dpiby5Qx.js → fp_explain-D6QvDLKQ.js} +1 -1
  9. package/build/{fp_inventory-DHwZzEQf.js → fp_inventory-C43fU39x.js} +1 -1
  10. package/build/{fp_metrics-M84qLYaE.js → fp_metrics-DEMPk4xC.js} +1 -1
  11. package/build/index.d.ts +8 -4
  12. package/build/index.js +4 -4
  13. package/build/{pipeline-Dm9KvUvF.js → pipeline-Cq4dNTNE.js} +841 -264
  14. package/build/{resolvers-vMahHkAd.js → resolvers-DlKJOZnk.js} +373 -63
  15. package/build/{runners-DetZGfh5.js → runners-FYmPIPub.js} +12 -4
  16. package/build/src/albrecht/counter.d.ts +31 -5
  17. package/build/src/albrecht/data_functions.d.ts +49 -3
  18. package/build/src/albrecht/diff.d.ts +27 -0
  19. package/build/src/albrecht/index.d.ts +1 -0
  20. package/build/src/albrecht/opaque.d.ts +90 -0
  21. package/build/src/albrecht/technical_filter.d.ts +18 -11
  22. package/build/src/albrecht/transactional_functions.d.ts +7 -0
  23. package/build/src/cli.js +2 -2
  24. package/build/src/define_config.d.ts +55 -57
  25. package/build/src/inventory/graph/call_graph.d.ts +39 -0
  26. package/build/src/inventory/graph/output_fields.d.ts +99 -0
  27. package/build/src/inventory/paths.d.ts +3 -0
  28. package/build/src/inventory/resolvers/index.d.ts +28 -0
  29. package/build/src/inventory/resolvers/index.js +2 -2
  30. package/build/src/inventory/resolvers/types.d.ts +19 -0
  31. package/build/src/pipeline.js +1 -1
  32. package/build/src/types.d.ts +32 -1
  33. package/build/stubs/config.stub +29 -16
  34. package/package.json +1 -1
@@ -40,6 +40,48 @@ const toPosix = (value) => value.split("\\").join("/");
40
40
  const relativeTo = (root, value) => toPosix(path.relative(toPosix(root), toPosix(value))) || ".";
41
41
  /** Compares two paths that may have come from different sources. */
42
42
  const samePath = (a, b) => a !== void 0 && b !== void 0 && toPosix(a) === toPosix(b);
43
+ /**
44
+ * Is this file the application's own code, as opposed to the scaffolding around it?
45
+ *
46
+ * The top-level filter on `scanRoots` already drops `tests/`, `database/` and the
47
+ * rest — but only at the ROOT. Applications organised by domain module put both
48
+ * inside `app/`:
49
+ *
50
+ * app/billing/tests/functional/invoice.spec.ts
51
+ * app/billing/seeders/plan_seeder.ts
52
+ *
53
+ * so they land in the project, and `writtenAnywhere()` read a seeder's inserts as
54
+ * the application maintaining the table. A reference table only the seed populates
55
+ * came out as an ILF — which the CPM does not allow: data maintained by the
56
+ * development team is at most an EIF, and code data is not counted at all.
57
+ *
58
+ * The segments are AdonisJS's own: `make:test` writes to a suite directory,
59
+ * `make:seeder` to `seeders`, `make:migration` to `migrations`, `make:factory` to
60
+ * `factories`. The `.spec`/`.test` suffixes come from the suite globs in
61
+ * `adonisrc.ts`.
62
+ */
63
+ const SCAFFOLDING = new Set([
64
+ "tests",
65
+ "test",
66
+ "seeders",
67
+ "seeder",
68
+ "migrations",
69
+ "factories"
70
+ ]);
71
+ /** a seeder, by the directory `make:seeder` writes to — scaffolding, but a fact the report uses */
72
+ function isSeeder(root, file) {
73
+ const relative = relativeTo(root, file);
74
+ if (relative.startsWith("..")) return false;
75
+ return relative.split("/").slice(0, -1).some((segment) => segment === "seeders" || segment === "seeder");
76
+ }
77
+ function isApplicationCode(root, file) {
78
+ const relative = relativeTo(root, file);
79
+ if (relative.startsWith("..")) return false;
80
+ const parts = relative.split("/");
81
+ const name = parts.at(-1) ?? "";
82
+ if (/\.(spec|test)\.[jt]s$/.test(name)) return false;
83
+ return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
84
+ }
43
85
  //#endregion
44
86
  //#region src/inventory/sources/event_bindings.ts
45
87
  /** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
@@ -714,81 +756,284 @@ const staticServiceResolver = {
714
756
  }
715
757
  };
716
758
  //#endregion
717
- //#region src/inventory/resolvers/transformer.ts
718
- /** BaseTransformer's public API; all of it funnels through `toObject` */
719
- const TRANSFORMER_METHODS = new Set([
720
- "transform",
721
- "paginate",
722
- "toJSON",
723
- "useVariant",
724
- "withVariant"
725
- ]);
726
- /** what the application-side body is called */
727
- const APPLICATION_BODY = "toObject";
759
+ //#region src/inventory/graph/output_fields.ts
728
760
  /**
729
- * "Transformer" pattern: the package supplies the API, the application
730
- * supplies the body.
731
- *
732
- * class InviteTransformer extends BaseTransformer<Invite> {
733
- * toObject() { … }
734
- * }
761
+ * Does this class extend a transformer base from a package?
735
762
  *
736
- * InviteTransformer.transform(invite)
763
+ * Decided by the base's name AND by its import being a bare specifier, so an
764
+ * application class that merely happens to own a `transform` method is not
765
+ * mistaken for one. Shared with the `transformer` resolver: one definition of
766
+ * what a transformer is, or the resolver follows a body this walker refuses.
767
+ */
768
+ function isTransformerClass(cls) {
769
+ const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
770
+ if (!name?.endsWith("Transformer")) return false;
771
+ const imported = cls.getSourceFile().getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
772
+ return imported !== void 0 && !imported.getModuleSpecifierValue().startsWith("#");
773
+ }
774
+ /** `class X extends BaseTransformer<Livro>` -> 'Livro' */
775
+ function transformerResourceOf(cls) {
776
+ return (cls.getExtends()?.getTypeArguments()[0])?.asKind(SyntaxKind.TypeReference)?.getTypeName().getText() ?? null;
777
+ }
778
+ /**
779
+ * The keys a transformer method returns.
737
780
  *
738
- * `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
739
- * symbol lands on the application file and finds no body there. The naive
740
- * reading is that the tracer must step into node_modules; it does not. Those
741
- * methods call BACK into `toObject()`, which the application writes, so the
742
- * body worth analysing was in the application all along.
781
+ * `followed` says whether a call inside the literal is a body the graph walks —
782
+ * `AutorTransformer.transform(x)`, `this.toObject()` — in which case its keys
783
+ * arrive through that body and the key holding it is not a DET of its own: the
784
+ * user sees the author's name, not an "autor" field.
743
785
  *
744
- * It is the same shape as `job-dispatch`, where `dispatch` enqueues and
745
- * `handle` executes.
786
+ * { titulo: l.titulo } 1 — `titulo`
787
+ * { autor: AutorTransformer.transform } 0 here; the nested body contributes
788
+ * { endereco: { rua, cidade } } leaves individually
789
+ * { tags: xs.map((t) => t.nome) } 1 — a repeating group of one attribute
790
+ * { itens: xs.map((i) => ({ a, b })) } the leaves, once
791
+ * ...this.pick(this.resource, [...]) the listed names
792
+ * ...this.toObject() 0 here; the followed body contributes
793
+ * ...anythingElse 1, opaque, reported
746
794
  *
747
- * COUNTING DECISION: the write a transformer performs belongs to the
748
- * transaction that serialised through it. Without this, a table written only
749
- * inside `toObject()` is reached by nobody and drops out under AFP §6.5.4.
795
+ * A key that is the identifier of the transformer's resource is not a DET, for
796
+ * the same reason `isPrimary` is not one on the data function.
750
797
  */
751
- const transformerResolver = {
752
- name: "transformer",
753
- order: 18,
754
- resolve(call, ctx) {
755
- const expression = call.getExpression();
756
- if (!Node.isPropertyAccessExpression(expression)) return [];
757
- if (!TRANSFORMER_METHODS.has(expression.getName())) return [];
798
+ function outputFieldsIn(body, owner, stores, followed) {
799
+ if (!owner || !isTransformerClass(owner)) return {
800
+ outputs: [],
801
+ opaqueOutputs: [],
802
+ resource: null
803
+ };
804
+ const qualifier = owner.getName() ?? "Transformer";
805
+ const resource = transformerResourceOf(owner);
806
+ /**
807
+ * The resource's key and its system timestamps: re-emitted for links and
808
+ * sorting, and not something the user recognises — the same two exclusions the
809
+ * data function applies (§6).
810
+ */
811
+ const excluded = new Set((resource ? stores.get(resource)?.attributes : void 0)?.filter((attribute) => attribute.isIdentifier || attribute.system).map((attribute) => attribute.name) ?? []);
812
+ const outputs = /* @__PURE__ */ new Set();
813
+ const opaque = /* @__PURE__ */ new Set();
814
+ const leaf = (prefix, name) => {
815
+ if (prefix === "" && excluded.has(name)) return;
816
+ outputs.add(`${qualifier}.${prefix ? `${prefix}.${name}` : name}`);
817
+ };
818
+ const walk = (literal, prefix) => {
819
+ for (const property of literal.getProperties()) {
820
+ if (Node.isShorthandPropertyAssignment(property)) {
821
+ leaf(prefix, property.getName());
822
+ continue;
823
+ }
824
+ if (Node.isMethodDeclaration(property) || Node.isGetAccessorDeclaration(property)) {
825
+ leaf(prefix, property.getName());
826
+ continue;
827
+ }
828
+ if (Node.isSpreadAssignment(property)) {
829
+ spread(property.getExpression(), prefix);
830
+ continue;
831
+ }
832
+ if (!Node.isPropertyAssignment(property)) continue;
833
+ const name = property.getName().replace(/^['"]|['"]$/g, "");
834
+ const value = unwrap(property.getInitializer());
835
+ if (!value) {
836
+ leaf(prefix, name);
837
+ continue;
838
+ }
839
+ if (Node.isObjectLiteralExpression(value)) {
840
+ walk(value, prefix ? `${prefix}.${name}` : name);
841
+ continue;
842
+ }
843
+ if (Node.isCallExpression(value) && followed(value)) continue;
844
+ const mapped = mappedLiteralOf(value);
845
+ if (mapped) {
846
+ walk(mapped, prefix ? `${prefix}.${name}` : name);
847
+ continue;
848
+ }
849
+ leaf(prefix, name);
850
+ }
851
+ };
852
+ const spread = (expression, prefix) => {
853
+ const value = unwrap(expression);
854
+ if (!value) return;
855
+ if (Node.isObjectLiteralExpression(value)) {
856
+ walk(value, prefix);
857
+ return;
858
+ }
859
+ if (Node.isConditionalExpression(value)) {
860
+ spread(value.getWhenTrue(), prefix);
861
+ spread(value.getWhenFalse(), prefix);
862
+ return;
863
+ }
864
+ if (Node.isCallExpression(value)) {
865
+ const picked = pickedNamesOf(value);
866
+ if (picked) {
867
+ for (const name of picked) leaf(prefix, name);
868
+ return;
869
+ }
870
+ if (followed(value)) return;
871
+ }
758
872
  /**
759
- * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
760
- * well as `X.transform(p)`. Analysing the same body twice is free: the
761
- * graph dedupes by file and member.
873
+ * `...this.resource.serialize()`, `...this.extras`: whatever the model has.
874
+ * One DET as a floor, and reported — the placeholder carries the expression
875
+ * so the report can name what could not be read.
762
876
  */
763
- const symbol = rootSymbolOf(expression.getExpression());
764
- if (!symbol) return [];
765
- const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
766
- if (!file) return [];
767
- const declared = ctx.sourceFile(file);
768
- if (!declared || !extendsTransformer(declared)) return [];
769
- return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
770
- file,
771
- member: APPLICATION_BODY
772
- }] : [];
877
+ const placeholder = `${qualifier}.${prefix ? `${prefix}.` : ""}<${value.getText().replace(/\s+/g, "")}>`;
878
+ outputs.add(placeholder);
879
+ opaque.add(placeholder);
880
+ };
881
+ for (const literal of returnedLiteralsOf(body)) walk(literal, "");
882
+ return {
883
+ outputs: [...outputs],
884
+ opaqueOutputs: [...opaque],
885
+ resource: resource && stores.has(resource) ? resource : null
886
+ };
887
+ }
888
+ /**
889
+ * Object literals the body itself returns — not the ones returned by arrow
890
+ * functions inside it, which belong to `.map()` callbacks and are read as
891
+ * repeating groups where they occur.
892
+ */
893
+ function returnedLiteralsOf(body) {
894
+ const literals = [];
895
+ for (const statement of body.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
896
+ if (statement.getFirstAncestor((node) => Node.isArrowFunction(node) || Node.isFunctionExpression(node) || Node.isMethodDeclaration(node) || Node.isFunctionDeclaration(node)) !== body) continue;
897
+ const value = unwrap(statement.getExpression());
898
+ if (value && Node.isObjectLiteralExpression(value)) literals.push(value);
773
899
  }
774
- };
900
+ return literals;
901
+ }
902
+ /** `this.pick(this.resource, ['a', 'b'])` -> ['a', 'b']; null when it is not that call */
903
+ function pickedNamesOf(call) {
904
+ const callee = call.getExpression();
905
+ if (!Node.isPropertyAccessExpression(callee)) return null;
906
+ if (callee.getName() !== "pick") return null;
907
+ if (callee.getExpression().getKind() !== SyntaxKind.ThisKeyword) return null;
908
+ const list = call.getArguments()[1]?.asKind(SyntaxKind.ArrayLiteralExpression);
909
+ if (!list) return null;
910
+ const names = [];
911
+ for (const element of list.getElements()) {
912
+ const name = element.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
913
+ if (name === void 0) return null;
914
+ names.push(name);
915
+ }
916
+ return names;
917
+ }
918
+ /** `xs.map((x) => ({ a, b }))` -> the literal; null for a scalar map or anything else */
919
+ function mappedLiteralOf(value) {
920
+ if (!Node.isCallExpression(value)) return null;
921
+ const callee = value.getExpression();
922
+ if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "map") return null;
923
+ const callback = value.getArguments()[0];
924
+ if (!callback || !Node.isArrowFunction(callback)) return null;
925
+ const returned = unwrap(callback.getBody().asKind(SyntaxKind.Block) ? null : callback.getBody());
926
+ if (returned && Node.isObjectLiteralExpression(returned)) return returned;
927
+ for (const statement of callback.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
928
+ const expression = unwrap(statement.getExpression());
929
+ if (expression && Node.isObjectLiteralExpression(expression)) return expression;
930
+ }
931
+ return null;
932
+ }
933
+ /** strips parentheses, `as`, `satisfies` and non-null assertions */
934
+ function unwrap(node) {
935
+ let current = node;
936
+ while (current && (Node.isParenthesizedExpression(current) || Node.isAsExpression(current) || Node.isSatisfiesExpression(current) || Node.isNonNullExpression(current))) current = current.getExpression();
937
+ return current && Node.isExpression(current) ? current : null;
938
+ }
775
939
  /**
776
- * Does a class here extend a transformer base from a package?
940
+ * The columns a fluent chain names in `.select()`, walking UP from the access
941
+ * the detector recognised (`Livro.query()`) through the chain it belongs to.
777
942
  *
778
- * Checked by the base's name and by its import being a bare specifier, so an
779
- * application class that merely happens to own a `transform` method is not
780
- * mistaken for one.
943
+ * Only calls on the chain itself qualify. A `q.select('nome')` inside a
944
+ * `preload('autor', (q) => …)` callback narrows the related store, not this
945
+ * one, and reading every descendant would have attributed it here.
946
+ *
947
+ * Both spellings count: `.select(['a', 'b'])` and `.select('a', 'b')`. Anything
948
+ * that is not a string literal — a variable, a raw expression — is unreadable:
949
+ * the store falls back to every column, and the chain is reported.
781
950
  */
782
- function extendsTransformer(file) {
783
- if (!file) return false;
784
- for (const cls of file.getClasses()) {
785
- const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
786
- if (!name?.endsWith("Transformer")) continue;
787
- const imported = file.getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
788
- if (imported && !imported.getModuleSpecifierValue().startsWith("#")) return true;
951
+ /** methods that leave ONE derived scalar rather than rows */
952
+ const AGGREGATES = new Set([
953
+ "count",
954
+ "countDistinct",
955
+ "exists",
956
+ "sum",
957
+ "avg",
958
+ "min",
959
+ "max"
960
+ ]);
961
+ function chainShapeOf(access) {
962
+ const selected = /* @__PURE__ */ new Set();
963
+ const unreadable = [];
964
+ let aggregate = false;
965
+ const inspect = (call) => {
966
+ const callee = call.getExpression();
967
+ if (!Node.isPropertyAccessExpression(callee)) return;
968
+ const name = callee.getName();
969
+ if (AGGREGATES.has(name)) aggregate = true;
970
+ if (name !== "select") return;
971
+ const literal = literalColumnsOf(call);
972
+ if (literal) for (const column of literal) selected.add(column);
973
+ else unreadable.push({
974
+ line: call.getStartLineNumber(),
975
+ expression: call.getText().replace(/\s+/g, "")
976
+ });
977
+ };
978
+ let node = access;
979
+ for (let depth = 0; node && depth < 40; depth++) {
980
+ if (Node.isCallExpression(node)) inspect(node);
981
+ node = Node.isCallExpression(node) || Node.isPropertyAccessExpression(node) ? node.getExpression() : void 0;
789
982
  }
790
- return false;
983
+ node = access;
984
+ for (let depth = 0; depth < 40; depth++) {
985
+ const parent = node.getParent();
986
+ if (!parent) break;
987
+ if (Node.isAwaitExpression(parent) || Node.isParenthesizedExpression(parent)) {
988
+ node = parent;
989
+ continue;
990
+ }
991
+ if (!Node.isPropertyAccessExpression(parent) || parent.getExpression() !== node) break;
992
+ const call = parent.getParent();
993
+ if (!call || !Node.isCallExpression(call) || call.getExpression() !== parent) break;
994
+ inspect(call);
995
+ node = call;
996
+ }
997
+ return {
998
+ selected: [...selected],
999
+ aggregate,
1000
+ unreadable
1001
+ };
791
1002
  }
1003
+ /** string-literal columns of one `.select(...)`; null when any is not a literal */
1004
+ function literalColumnsOf(call) {
1005
+ const columns = [];
1006
+ const read = (node) => {
1007
+ const value = node.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
1008
+ if (value === void 0) return false;
1009
+ const bare = value.includes(".") ? value.split(".").pop() : value;
1010
+ if (bare !== "*") columns.push(camelCase(bare));
1011
+ return true;
1012
+ };
1013
+ for (const argument of call.getArguments()) {
1014
+ const list = argument.asKind(SyntaxKind.ArrayLiteralExpression);
1015
+ if (list) {
1016
+ for (const element of list.getElements()) if (!read(element)) return null;
1017
+ continue;
1018
+ }
1019
+ if (!read(argument)) return null;
1020
+ }
1021
+ return columns;
1022
+ }
1023
+ /** `created_at` -> `createdAt`, to match the model's attribute names */
1024
+ const camelCase = (value) => value.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
1025
+ //#endregion
1026
+ //#region src/inventory/resolvers/transformer.ts
1027
+ /** BaseTransformer's public API; all of it funnels through `toObject` */
1028
+ const TRANSFORMER_METHODS = new Set([
1029
+ "transform",
1030
+ "paginate",
1031
+ "toJSON",
1032
+ "useVariant",
1033
+ "withVariant"
1034
+ ]);
1035
+ /** what the application-side body is called */
1036
+ const APPLICATION_BODY = "toObject";
792
1037
  //#endregion
793
1038
  //#region src/inventory/resolvers/index.ts
794
1039
  /**
@@ -803,7 +1048,35 @@ const BUILTIN_CALL_RESOLVERS = [
803
1048
  actionObjectResolver,
804
1049
  eventDispatchResolver,
805
1050
  jobDispatchResolver,
806
- transformerResolver,
1051
+ {
1052
+ name: "transformer",
1053
+ order: 18,
1054
+ resolve(call, ctx) {
1055
+ const expression = call.getExpression();
1056
+ 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 [];
1067
+ /**
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.
1071
+ */
1072
+ const declared = ctx.sourceFile(file);
1073
+ if (!declared || !declared.getClasses().some(isTransformerClass)) return [];
1074
+ return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
1075
+ file,
1076
+ member: APPLICATION_BODY
1077
+ }] : [];
1078
+ }
1079
+ },
807
1080
  staticServiceResolver,
808
1081
  propertyServiceResolver,
809
1082
  moduleFunctionResolver
@@ -817,6 +1090,15 @@ const BUILTIN_CALL_RESOLVERS = [
817
1090
  * them apart. Hence specific strategies declare a lower `order` than generic
818
1091
  * ones, and `module-function` comes last — it would match almost anything.
819
1092
  */
1093
+ /**
1094
+ * Does any strategy call this a technical write?
1095
+ *
1096
+ * Asked separately from resolution, because the strategy that recognises the call as
1097
+ * incidental is not necessarily the one that knows where it goes.
1098
+ */
1099
+ function isTechnicalWrite(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
1100
+ return resolvers.some((resolver) => resolver.technicalWrite?.(call, ctx) === true);
1101
+ }
820
1102
  function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
821
1103
  for (const resolver of resolvers) {
822
1104
  /**
@@ -836,5 +1118,33 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
836
1118
  }
837
1119
  return null;
838
1120
  }
1121
+ /**
1122
+ * A strategy that recognises a family of calls and knows they reach no data
1123
+ * store — a rate limiter, an attachment's URL, an authorisation check.
1124
+ *
1125
+ * Read from a real configuration, every such strategy was the same eight lines:
1126
+ * a helper to get the method name off the ts-morph node (the app does not depend
1127
+ * on ts-morph), a `resolve` that returns nothing, and one comparison. What the
1128
+ * design wants is kept — it is still a NAMED strategy, and `fp:count` still
1129
+ * reports the volume it declared data-free — and the ceremony is not.
1130
+ */
1131
+ function ignoreCalls(options) {
1132
+ if (!options.methods?.length && !options.matching) throw new Error(`ignoreCalls("${options.name}"): say what it ignores — \`methods\` or \`matching\``);
1133
+ const methods = new Set(options.methods ?? []);
1134
+ return {
1135
+ name: options.name,
1136
+ order: options.order ?? 1,
1137
+ /** follows nothing: the whole point */
1138
+ resolve: () => [],
1139
+ ignores(call) {
1140
+ const callee = call.getExpression();
1141
+ const text = callee.getText();
1142
+ if (options.matching?.test(text)) return true;
1143
+ if (methods.size === 0) return false;
1144
+ const method = callee.getKindName() === "PropertyAccessExpression" ? text.split(".").pop() : text;
1145
+ return method !== void 0 && methods.has(method);
1146
+ }
1147
+ };
1148
+ }
839
1149
  //#endregion
840
- export { rootSymbolOf as a, samePath as c, hooksFiredBy as i, toPosix as l, resolveCall as n, collectEventBindings as o, detectAccess as r, relativeTo as s, BUILTIN_CALL_RESOLVERS as t };
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 };
@@ -1,6 +1,6 @@
1
- import { c as diffCounts, i as measureStructure, l as DEFAULTS, n as parseSamples, o as IncomparableRulesetsError, r as measureConformance, s as IncomparableSourcesError, t as calibrate, u as defineConfig } from "./calibration-8eV8CEix.js";
2
- import { l as toPosix } from "./resolvers-vMahHkAd.js";
3
- import { n as analyze } from "./pipeline-Dm9KvUvF.js";
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";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { existsSync } from "node:fs";
@@ -91,7 +91,12 @@ function renderCount(result) {
91
91
  * habit: if it grows, the count comes from a spreadsheet and the tool loses
92
92
  * its reason to exist. Printing the share is what keeps that visible.
93
93
  */
94
- const overridden = result.functions.filter((fn) => fn.rationale.overrides?.length);
94
+ /**
95
+ * Only entries that DECLARED a number. A review records a decision and declares
96
+ * nothing, so counting it here would read as "35% of the total declared by
97
+ * override" about a count nobody touched.
98
+ */
99
+ const overridden = result.functions.filter((fn) => fn.rationale.overrides?.some((o) => o.fields.length > 0));
95
100
  if (overridden.length > 0) {
96
101
  const points = overridden.reduce((total, fn) => total + fn.points, 0);
97
102
  const share = (points / (result.totals.unadjusted || 1) * 100).toFixed(1);
@@ -220,6 +225,8 @@ function renderDiff(diff) {
220
225
  }
221
226
  lines.push("");
222
227
  lines.push(`Billable FP: ${diff.billable}`);
228
+ /** the total is quoted under a set of factors, so the set is named beside it */
229
+ lines.push(`Factors: ${diff.preset} — ${FACTOR_PRESETS[diff.preset].label}`);
223
230
  /**
224
231
  * Before the per-function list, not after it.
225
232
  *
@@ -410,6 +417,7 @@ async function runDiff(options) {
410
417
  from: options.previous,
411
418
  to
412
419
  },
420
+ preset: config.diff?.preset,
413
421
  factors: config.diff?.factors,
414
422
  reasonFactors: config.diff?.reasonFactors
415
423
  }))
@@ -3,9 +3,12 @@ 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 { OpaqueDeclaration } from './opaque.js';
6
7
  import type { Complexity, CountResult, FunctionType } from '../types.js';
7
8
  import type { FunctionOverride } from '../define_config.js';
8
9
  import type { ComplexityTable } from './tables.js';
10
+ import type { GroupingStrategy } from './data_functions.js';
11
+ import type { TechnicalPattern } from './technical_filter.js';
9
12
  /**
10
13
  * Assembles the count from the inventory.
11
14
  *
@@ -26,15 +29,19 @@ export declare const RULESET = "afp";
26
29
  * that is easy to forget. Four such changes landed in 1.1.0 — maintenance read
27
30
  * across the whole project rather than from routes alone, a job followed into
28
31
  * `process`, an event followed into its listeners, and `request.input(…)` counted
29
- * as a DET — and three more in 1.2.0: an open input object counting 1 instead of 0,
32
+ * as a DET — three more in 1.2.0: an open input object counting 1 instead of 0,
30
33
  * `detFromSchema` no longer subtracting a placeholder that was not there, and a
31
- * write through `related(…)` maintaining the related table.
34
+ * write through `related(…)` maintaining the related table — and six in 1.5.0:
35
+ * output DETs read from transformers, selects and aggregates instead of every
36
+ * column; system timestamps and `serializeAs: null` columns leaving the DETs;
37
+ * master-detail folded into one data function; identity by table; token tables
38
+ * technical; and opaque declarations reaching every function carrying the origin.
32
39
  *
33
40
  * Without the bump, a baseline saved by the previous version compares cleanly
34
41
  * against this one and bills the tool's own improvement as work done. The guard
35
42
  * exists for exactly that, and only this constant arms it.
36
43
  */
37
- export declare const RULESET_VERSION = "1.3.0";
44
+ export declare const RULESET_VERSION = "1.5.0";
38
45
  export type CountInput = {
39
46
  app: AppContext;
40
47
  stores: CollectedDataStore[];
@@ -49,16 +56,35 @@ export type CountInput = {
49
56
  * seeder is this application just as much as a route is.
50
57
  */
51
58
  writtenAnywhere?: Set<string>;
59
+ /**
60
+ * Stores the application addresses DIRECTLY somewhere in its code — as opposed
61
+ * to reaching only through a parent's relation. Decides which composition
62
+ * children fold into their parent as a RET (counting-decisions §10).
63
+ */
64
+ addressedAnywhere?: Set<string>;
65
+ /**
66
+ * Stores written by a SEEDER — scaffolding, so not maintenance — kept apart
67
+ * because an EIF only a seed populates is one of two things the code cannot
68
+ * tell: code data the team maintains (not counted, CPM) or a mirror of data
69
+ * another system maintains in production (a legitimate EIF). Reported.
70
+ */
71
+ seededAnywhere?: Set<string>;
52
72
  };
53
73
  export type CountOptions = {
54
- retStrategy?: 'constant' | 'composition';
55
- /** declared DET/RET for what static analysis cannot read — see §8 */
74
+ dataFunctions?: {
75
+ grouping?: GroupingStrategy;
76
+ };
77
+ /** what a person declared about a DET the analysis cannot read, by origin — §8 */
78
+ opaque?: Record<string, OpaqueDeclaration>;
79
+ /** a declared DET or RET for one function — the last resort, see §8 */
56
80
  overrides?: Record<string, FunctionOverride>;
57
81
  boundary?: {
58
82
  infrastructure?: string[];
59
83
  externallyMaintained?: string[];
60
84
  /** restores what the AFP naming filter caught by accident */
61
85
  business?: string[];
86
+ /** replaces the filter's naming conventions — §6.5.2.1.3 treats them as user input */
87
+ technicalPatterns?: TechnicalPattern[];
62
88
  ignoreEntryPoints?: string[];
63
89
  };
64
90
  messageDet?: number;
@@ -13,7 +13,9 @@ import type { ComplexityTable } from './tables.js';
13
13
  * application's Transactional Functions, the Data Function shall not be
14
14
  * counted in the application." — AFP §6.5.4
15
15
  *
16
- * That is why this module takes usage, not just the stores.
16
+ * That is why this module takes usage, not just the stores. And the same
17
+ * question — how does the application use it? — decides whether a table is a
18
+ * data function at all or a RET of another one (counting-decisions §10).
17
19
  */
18
20
  export type StoreUsage = {
19
21
  /** does any transaction of the application write to this store? */
@@ -21,9 +23,53 @@ export type StoreUsage = {
21
23
  /** does any transaction reach it at all, reading or writing? */
22
24
  used: boolean;
23
25
  };
26
+ export type GroupingStrategy = 'usage' | 'none';
27
+ export type GroupingOptions = {
28
+ /**
29
+ * `usage` folds a composition child nobody addresses directly into its parent
30
+ * as a RET; `none` keeps every table its own data function, RET 1 — the
31
+ * behaviour of rule sets before 1.5.0, for comparing with an old count.
32
+ */
33
+ grouping: GroupingStrategy;
34
+ /**
35
+ * Stores the application addresses directly anywhere in its own code —
36
+ * `C.query()`, `C.create()`, `new C()` — as opposed to reaching only through
37
+ * a parent's relation. Same pass as `writtenAnywhere`, same exclusions.
38
+ */
39
+ addressedAnywhere: Set<string>;
40
+ };
41
+ /**
42
+ * How the stores fold into data functions.
43
+ *
44
+ * rootOf every counted store -> the store whose data function it belongs to
45
+ * members root -> [root, ...children folded in]
46
+ * linkColumns store -> the foreign keys that are the subgroup's LINK to its
47
+ * parent, and therefore not DETs of the group
48
+ */
49
+ export type StoreGrouping = {
50
+ strategy: GroupingStrategy;
51
+ rootOf: Map<string, string>;
52
+ members: Map<string, string[]>;
53
+ linkColumns: Map<string, Set<string>>;
54
+ warnings: string[];
55
+ };
56
+ /**
57
+ * A store `C` is a RET of `P` when, and only when:
58
+ *
59
+ * 1. `P` declares `hasMany` / `hasOne` -> `C` (collected as `subgroups`);
60
+ * 2. no application code addresses `C` directly — the user only ever reaches
61
+ * it through `P`, so under the CPM it is not a logical file of its own;
62
+ * 3. exactly one `P` satisfies (1). More than one: `C` stays apart, reported.
63
+ *
64
+ * Cascade delete was measured and rejected as the signal: on a real application
65
+ * 11 of 13 cascades pointed at the tenant table. Usage is the rule the rest of
66
+ * the count already runs on.
67
+ */
68
+ export declare function groupStores(stores: CollectedDataStore[], options: GroupingOptions): StoreGrouping;
69
+ /** the DET attributes of one store: not the key, not a system stamp, not a link to its parent */
70
+ export declare function detAttributesOf(store: CollectedDataStore, links?: Set<string>): import("../types.js").Attribute[];
24
71
  export type DataFunctionOptions = {
25
- /** `constant` pins RET at 1; `composition` derives it from composition relations */
26
- retStrategy: 'constant' | 'composition';
72
+ grouping: StoreGrouping;
27
73
  /** stores maintained by another system, by boundary decision */
28
74
  externallyMaintained: Set<string>;
29
75
  /**