@filipebraida/adonis-function-points 0.5.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 (33) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +101 -292
  3. package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
  4. package/build/commands/main.js +6 -6
  5. package/build/{fp_calibrate-DUbHiifm.js → fp_calibrate-EAuAtdbq.js} +1 -1
  6. package/build/{fp_count-ChtblhZV.js → fp_count-CZ0cUUBQ.js} +1 -1
  7. package/build/{fp_diff-Dt7J4IWu.js → fp_diff-BTg_LX0r.js} +1 -1
  8. package/build/{fp_explain-DZJ--0-S.js → fp_explain-D6QvDLKQ.js} +1 -1
  9. package/build/{fp_inventory-CPtmuuke.js → fp_inventory-C43fU39x.js} +1 -1
  10. package/build/{fp_metrics-et8F1Wvt.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-CNTBhs6o.js → pipeline-Cq4dNTNE.js} +763 -313
  14. package/build/{resolvers-PJwo2Z8R.js → resolvers-DlKJOZnk.js} +328 -63
  15. package/build/{runners-DIt1G85i.js → runners-FYmPIPub.js} +6 -3
  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 +29 -0
  26. package/build/src/inventory/graph/output_fields.d.ts +99 -0
  27. package/build/src/inventory/paths.d.ts +2 -0
  28. package/build/src/inventory/resolvers/index.d.ts +21 -0
  29. package/build/src/inventory/resolvers/index.js +2 -2
  30. package/build/src/pipeline.js +1 -1
  31. package/build/src/types.d.ts +30 -1
  32. package/build/stubs/config.stub +29 -16
  33. package/package.json +1 -1
@@ -68,6 +68,12 @@ const SCAFFOLDING = new Set([
68
68
  "migrations",
69
69
  "factories"
70
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
+ }
71
77
  function isApplicationCode(root, file) {
72
78
  const relative = relativeTo(root, file);
73
79
  if (relative.startsWith("..")) return false;
@@ -750,81 +756,284 @@ const staticServiceResolver = {
750
756
  }
751
757
  };
752
758
  //#endregion
753
- //#region src/inventory/resolvers/transformer.ts
754
- /** BaseTransformer's public API; all of it funnels through `toObject` */
755
- const TRANSFORMER_METHODS = new Set([
756
- "transform",
757
- "paginate",
758
- "toJSON",
759
- "useVariant",
760
- "withVariant"
761
- ]);
762
- /** what the application-side body is called */
763
- const APPLICATION_BODY = "toObject";
759
+ //#region src/inventory/graph/output_fields.ts
764
760
  /**
765
- * "Transformer" pattern: the package supplies the API, the application
766
- * supplies the body.
767
- *
768
- * class InviteTransformer extends BaseTransformer<Invite> {
769
- * toObject() { … }
770
- * }
761
+ * Does this class extend a transformer base from a package?
771
762
  *
772
- * 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.
773
780
  *
774
- * `transform()` and `paginate()` live in `@adonisjs/core`, so resolving the
775
- * symbol lands on the application file and finds no body there. The naive
776
- * reading is that the tracer must step into node_modules; it does not. Those
777
- * methods call BACK into `toObject()`, which the application writes, so the
778
- * 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.
779
785
  *
780
- * It is the same shape as `job-dispatch`, where `dispatch` enqueues and
781
- * `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
782
794
  *
783
- * COUNTING DECISION: the write a transformer performs belongs to the
784
- * transaction that serialised through it. Without this, a table written only
785
- * 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.
786
797
  */
787
- const transformerResolver = {
788
- name: "transformer",
789
- order: 18,
790
- resolve(call, ctx) {
791
- const expression = call.getExpression();
792
- if (!Node.isPropertyAccessExpression(expression)) return [];
793
- 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
+ }
794
872
  /**
795
- * The root of the chain, so `X.transform(p).useVariant(v)` resolves as
796
- * well as `X.transform(p)`. Analysing the same body twice is free: the
797
- * 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.
798
876
  */
799
- const symbol = rootSymbolOf(expression.getExpression());
800
- if (!symbol) return [];
801
- const file = ctx.imports.get(symbol) ?? ctx.injected.get(symbol);
802
- if (!file) return [];
803
- const declared = ctx.sourceFile(file);
804
- if (!declared || !extendsTransformer(declared)) return [];
805
- return declared.getClasses().find((cls) => cls.getMethod(APPLICATION_BODY)) ? [{
806
- file,
807
- member: APPLICATION_BODY
808
- }] : [];
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);
809
899
  }
810
- };
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
+ }
811
939
  /**
812
- * 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.
813
942
  *
814
- * Checked by the base's name and by its import being a bare specifier, so an
815
- * application class that merely happens to own a `transform` method is not
816
- * 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.
817
950
  */
818
- function extendsTransformer(file) {
819
- if (!file) return false;
820
- for (const cls of file.getClasses()) {
821
- const name = (cls.getExtends()?.getExpression())?.asKind(SyntaxKind.Identifier)?.getText();
822
- if (!name?.endsWith("Transformer")) continue;
823
- const imported = file.getImportDeclarations().find((declaration) => declaration.getNamedImports().some((named) => named.getName() === name));
824
- 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;
825
982
  }
826
- 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
+ };
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;
827
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";
828
1037
  //#endregion
829
1038
  //#region src/inventory/resolvers/index.ts
830
1039
  /**
@@ -839,7 +1048,35 @@ const BUILTIN_CALL_RESOLVERS = [
839
1048
  actionObjectResolver,
840
1049
  eventDispatchResolver,
841
1050
  jobDispatchResolver,
842
- 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
+ },
843
1080
  staticServiceResolver,
844
1081
  propertyServiceResolver,
845
1082
  moduleFunctionResolver
@@ -881,5 +1118,33 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
881
1118
  }
882
1119
  return null;
883
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
+ }
884
1149
  //#endregion
885
- export { hooksFiredBy as a, isApplicationCode as c, toPosix as d, detectAccess as i, relativeTo as l, isTechnicalWrite as n, rootSymbolOf as o, resolveCall as r, collectEventBindings as s, BUILTIN_CALL_RESOLVERS as t, samePath as u };
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 { d as toPosix } from "./resolvers-PJwo2Z8R.js";
3
- import { n as analyze } from "./pipeline-CNTBhs6o.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";
@@ -225,6 +225,8 @@ function renderDiff(diff) {
225
225
  }
226
226
  lines.push("");
227
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}`);
228
230
  /**
229
231
  * Before the per-function list, not after it.
230
232
  *
@@ -415,6 +417,7 @@ async function runDiff(options) {
415
417
  from: options.previous,
416
418
  to
417
419
  },
420
+ preset: config.diff?.preset,
418
421
  factors: config.diff?.factors,
419
422
  reasonFactors: config.diff?.reasonFactors
420
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.4.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
  /**
@@ -37,6 +37,29 @@ export declare class IncomparableRulesetsError extends Error {
37
37
  */
38
38
  export type ChangeFactors = Record<ChangeType, number>;
39
39
  export declare const AEP_FACTORS: ChangeFactors;
40
+ /**
41
+ * The Roteiro de Métricas de Software do SISP, v3.0 (Portaria SGD/MGI nº 3656,
42
+ * de 2026), §7.3 "Projeto de Melhoria" — what a Brazilian public contract names
43
+ * instead of AEP:
44
+ *
45
+ * PF_MELHORIA = PF_INCLUÍDO + FI × PF_ALTERADO + 0,50 × PF_EXCLUÍDO + PF_CONVERSÃO
46
+ *
47
+ * where FI, the impact factor on an altered function, is 63% when the contractor
48
+ * developed or already maintains the function, and 84% when it did not (and must
49
+ * document it). This preset carries the 63% — a factory billing maintenance of
50
+ * its own work — and `diff.factors: { changed: 0.84 }` is the other case.
51
+ * PF_CONVERSÃO is data conversion, which this package does not count.
52
+ *
53
+ * Read from the guide's own PDF, not from memory: an earlier draft of this
54
+ * preset said 0,50 / 0,30, and v2.0 (2012) priced exclusion at 0,40. A contract
55
+ * binds to a revision, so the report prints which preset priced the total.
56
+ */
57
+ export declare const SISP_FACTORS: ChangeFactors;
58
+ export type FactorPreset = 'aep' | 'sisp';
59
+ export declare const FACTOR_PRESETS: Record<FactorPreset, {
60
+ label: string;
61
+ factors: ChangeFactors;
62
+ }>;
40
63
  /**
41
64
  * Factors for a modified function, by WHAT changed about it.
42
65
  *
@@ -52,6 +75,8 @@ export declare const AEP_FACTORS: ChangeFactors;
52
75
  */
53
76
  export type ChangeReasonFactors = Partial<Record<ChangeReason, number>>;
54
77
  export type DiffOptions = {
78
+ /** which published set of factors to start from; `factors` overrides it field by field */
79
+ preset?: FactorPreset;
55
80
  factors?: Partial<ChangeFactors>;
56
81
  /** per-reason factors for modified functions; each falls back to `factors.changed` */
57
82
  reasonFactors?: ChangeReasonFactors;
@@ -64,6 +89,8 @@ export type DiffOptions = {
64
89
  export type FunctionPointDiff = DiffResult & {
65
90
  /** function points weighted by the factors — this is what gets billed */
66
91
  billable: number;
92
+ /** the preset the factors started from — printed, because the total is quoted under it */
93
+ preset: FactorPreset;
67
94
  factors: ChangeFactors;
68
95
  /** what the modified functions were actually billed at, by reason */
69
96
  reasonFactors: ChangeReasonFactors;
@@ -8,6 +8,7 @@
8
8
  * Normative reference: OMG Automated Function Points 1.0 / ISO/IEC 19515.
9
9
  */
10
10
  export * from './tables.js';
11
+ export * from './technical_filter.js';
11
12
  export * from './counter.js';
12
13
  export * from './diff.js';
13
14
  export * from './calibration.js';