@filipebraida/adonis-function-points 0.9.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,46 @@ release moves the number for unchanged code, the rule set version moves with it
6
6
  otherwise the difference would measure the tool's change rather than the work, and
7
7
  that difference becomes an invoice.
8
8
 
9
+ ## 0.10.0
10
+
11
+ **Rule set `afp@1.9.0`.** One rule and two report fixes, from a team's third review of their
12
+ own count — the one they accepted as a baseline. The rule moves no point on the three
13
+ validated applications and no point on its fixture: it moves a gap from the wrong line to
14
+ the right one. A 0.9.0 baseline still has to be recounted, because the rule set says so.
15
+
16
+ ### Fixed
17
+
18
+ - **A warning names a transaction the way the table and the config keys do.** Four warnings
19
+ printed the route's own pattern (`GET /orders/:id`) where the table, `fp:explain` and the
20
+ keys of `overrides` / `boundary.ignoreEntryPoints` use the identity (`GET /orders/:param`,
21
+ §5). Copying from a warning into the configuration required a translation nobody was told
22
+ about. All of them print the identity now.
23
+ - **"leaves whole — page never reads X" contradicted itself.** The rule was right — a store
24
+ handed to a page that uses nothing of it the reader can see leaves whole, in the open, never
25
+ as a floor — and the sentence was not. It says now: "handed to page "x", and nothing the
26
+ reader can see uses X: every column counted, in the open".
27
+
28
+ ### Counting
29
+
30
+ - **The value of a transaction callback is what it returns.** `const { row } = await
31
+ db.transaction(async (trx) => { … return { row: created } })`, then `row.save()`: the write
32
+ was an unreadable receiver. The callback is a body — its own locals are bound first, in
33
+ source order — and every `return` names the store the value holds, whole or under one key
34
+ of a returned literal; `Model.transaction` and `trx.transaction` are the same shape. A
35
+ returned number binds nothing; a raw-query row nobody can type stays reported. On the three
36
+ applications the value of a transaction callback is used 17 times. The 0.8 rule "a value a
37
+ package built is not a store" narrows to a call with no function argument: `db` is a
38
+ package import, and `db.transaction(cb)` hands back the application's value.
39
+
40
+ ### Documented
41
+
42
+ - counting-decisions §3 gains the transaction-callback row of the bindings table.
43
+ - Fixtures: `transacao_valor` (18 FP — the same before and after; one unresolved call moves
44
+ from `row.save` to the raw-query row that deserves it); `inertia_pages` grows a page that
45
+ uses nothing of its rows (29 FP).
46
+ - Measured and set aside: a model's instance method that writes — one in 52 models on the
47
+ three applications, not a rule.
48
+
9
49
  ## 0.9.0
10
50
 
11
51
  **Rule set `afp@1.8.0`.** Two rules from a team's second review of their own count, one
package/README.md CHANGED
@@ -13,7 +13,7 @@ node ace fp:count
13
13
 
14
14
  ```
15
15
  Unadjusted count: 46 FP
16
- Ruleset: afp@1.8.0
16
+ Ruleset: afp@1.9.0
17
17
 
18
18
  type n FP
19
19
  ILF 2 14
@@ -14,27 +14,27 @@
14
14
  const commands = [
15
15
  {
16
16
  commandName: "fp:inventory",
17
- importer: () => import("../fp_inventory-B4Pv8Thz.js")
17
+ importer: () => import("../fp_inventory-BJlIoetL.js")
18
18
  },
19
19
  {
20
20
  commandName: "fp:metrics",
21
- importer: () => import("../fp_metrics-DoS5riGr.js")
21
+ importer: () => import("../fp_metrics-BDKaNL0B.js")
22
22
  },
23
23
  {
24
24
  commandName: "fp:count",
25
- importer: () => import("../fp_count-Bjviq8dz.js")
25
+ importer: () => import("../fp_count-B5hxDyGJ.js")
26
26
  },
27
27
  {
28
28
  commandName: "fp:explain",
29
- importer: () => import("../fp_explain-CrFcMeXU.js")
29
+ importer: () => import("../fp_explain-C4X_-Rp7.js")
30
30
  },
31
31
  {
32
32
  commandName: "fp:diff",
33
- importer: () => import("../fp_diff-BSK1kKLQ.js")
33
+ importer: () => import("../fp_diff-7S8NcKIY.js")
34
34
  },
35
35
  {
36
36
  commandName: "fp:calibrate",
37
- importer: () => import("../fp_calibrate-DJspMFHc.js")
37
+ importer: () => import("../fp_calibrate-DUPUXVaJ.js")
38
38
  }
39
39
  ];
40
40
  let cache = null;
@@ -1,4 +1,4 @@
1
- import { s as printResult, t as runCalibrate } from "./runners-BuZNr-FE.js";
1
+ import { s as printResult, t as runCalibrate } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, args } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_calibrate.ts
@@ -1,4 +1,4 @@
1
- import { n as runCount, s as printResult } from "./runners-BuZNr-FE.js";
1
+ import { n as runCount, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, flags } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_count.ts
@@ -1,4 +1,4 @@
1
- import { r as runDiff, s as printResult } from "./runners-BuZNr-FE.js";
1
+ import { r as runDiff, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, args } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_diff.ts
@@ -1,4 +1,4 @@
1
- import { i as runExplain, s as printResult } from "./runners-BuZNr-FE.js";
1
+ import { i as runExplain, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, args } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_explain.ts
@@ -1,4 +1,4 @@
1
- import { a as runInventory, s as printResult } from "./runners-BuZNr-FE.js";
1
+ import { a as runInventory, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, flags } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_inventory.ts
@@ -1,4 +1,4 @@
1
- import { o as runMetrics, s as printResult } from "./runners-BuZNr-FE.js";
1
+ import { o as runMetrics, s as printResult } from "./runners-b1tgLA1B.js";
2
2
  import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
3
3
  import { BaseCommand, flags } from "@adonisjs/core/ace";
4
4
  //#region commands/fp_metrics.ts
package/build/index.js CHANGED
@@ -2,5 +2,5 @@ import { configure } from "./configure.js";
2
2
  import { a as AEP_FACTORS, c as IncomparableSourcesError, f as defineConfig, i as measureStructure, l as SISP_FACTORS, n as parseSamples, o as FACTOR_PRESETS, r as measureConformance, s as IncomparableRulesetsError, t as calibrate, u as diffCounts } from "./calibration-DVIf8hcE.js";
3
3
  import "./src/types.js";
4
4
  import { n as ignoreCalls, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-CneCj3sT.js";
5
- import { a as DEFAULT_TECHNICAL_PATTERNS, i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-DqPrDZfD.js";
5
+ import { a as DEFAULT_TECHNICAL_PATTERNS, i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-4sZBYGKU.js";
6
6
  export { AEP_FACTORS, BUILTIN_CALL_RESOLVERS, CoverageTooLowError, DEFAULT_TECHNICAL_PATTERNS, FACTOR_PRESETS, IncomparableRulesetsError, IncomparableSourcesError, RULESET, RULESET_VERSION, SISP_FACTORS, analyze, calibrate, configure, defineConfig, diffCounts, ignoreCalls, measureConformance, measureStructure, parseSamples };
@@ -2168,7 +2168,7 @@ function readPages(deliveries, env) {
2168
2168
  for (const { store } of readable) {
2169
2169
  if (reading.columns.has(store) || reading.unreadable.has(store)) continue;
2170
2170
  const members = [...reading.unknownMembers.get(store) ?? []];
2171
- markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `page "${page}" never reads ${store}`);
2171
+ markUnreadable(reading, store, members.length > 0 ? `page "${page}" reads ${members.slice(0, 4).map((m) => `\`${m}\``).join(", ")} off ${store}, none of them a column of it` : `handed to page "${page}", and nothing the reader can see uses ${store}: every column counted, in the open`);
2172
2172
  }
2173
2173
  }
2174
2174
  return reading;
@@ -3065,6 +3065,59 @@ function createAnalyzer(app, stores, options = {}) {
3065
3065
  }
3066
3066
  return found.size === 1 ? [...found][0] : null;
3067
3067
  };
3068
+ /**
3069
+ * `await db.transaction(async (trx) => { … return … })` — also `Model.transaction`
3070
+ * and `trx.transaction` (a savepoint): a body whose VALUE is what it returns. The
3071
+ * callback's own locals are bound first, in source order, so `return { row: created }`
3072
+ * can be read; then every return names one store, or nothing binds (plan 0.10 §B).
3073
+ */
3074
+ const transactionCallbackOf = (call) => {
3075
+ const callee = call.getExpression();
3076
+ if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "transaction") return null;
3077
+ return call.getArguments().find((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a)) ?? null;
3078
+ };
3079
+ const returnsOf = (callback) => {
3080
+ if (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return [];
3081
+ const callbackBody = callback.getBody();
3082
+ if (!Node.isBlock(callbackBody)) return [callbackBody];
3083
+ const returned = [];
3084
+ for (const statement of callbackBody.getDescendantsOfKind(SyntaxKind.ReturnStatement)) {
3085
+ const owner = statement.getFirstAncestor((n) => Node.isArrowFunction(n) || Node.isFunctionExpression(n) || Node.isFunctionDeclaration(n) || Node.isMethodDeclaration(n));
3086
+ const expression = statement.getExpression();
3087
+ if (owner === callback && expression) returned.push(expression);
3088
+ }
3089
+ return returned;
3090
+ };
3091
+ const bindCallbackLocals = (callback) => {
3092
+ for (const declaration of callback.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
3093
+ const nameNode = declaration.getNameNode();
3094
+ const initializer = declaration.getInitializer();
3095
+ if (!initializer || !Node.isIdentifier(nameNode)) continue;
3096
+ const store = storeOfValue(initializer);
3097
+ if (store) symbols.set(nameNode.getText(), store);
3098
+ }
3099
+ };
3100
+ /** the store every return of the callback names — whole, or under one key of a returned literal */
3101
+ const transactionValueStore = (callback, key) => {
3102
+ bindCallbackLocals(callback);
3103
+ const returned = returnsOf(callback);
3104
+ if (returned.length === 0) return null;
3105
+ const found = /* @__PURE__ */ new Set();
3106
+ for (const expression of returned) {
3107
+ let value = unwrapAwait(expression);
3108
+ if (key) {
3109
+ if (!Node.isObjectLiteralExpression(value)) return null;
3110
+ const property = value.getProperty(key);
3111
+ if (!property) return null;
3112
+ value = Node.isPropertyAssignment(property) ? property.getInitializer() : Node.isShorthandPropertyAssignment(property) ? property.getNameNode() : void 0;
3113
+ if (!value) return null;
3114
+ }
3115
+ const store = storeOfValue(value);
3116
+ if (!store) return null;
3117
+ found.add(store);
3118
+ }
3119
+ return found.size === 1 ? [...found][0] : null;
3120
+ };
3068
3121
  const isAuthUser = (node) => {
3069
3122
  if (!authUserStore) return false;
3070
3123
  const chain = Node.isCallExpression(node) ? node.getExpression() : node;
@@ -3097,6 +3150,8 @@ function createAnalyzer(app, stores, options = {}) {
3097
3150
  if (Node.isPropertyAccessExpression(node)) return storeOfExpression(node, symbols, relationsByStore);
3098
3151
  if (Node.isCallExpression(node)) {
3099
3152
  const callee = node.getExpression();
3153
+ const callback = transactionCallbackOf(node);
3154
+ if (callback) return transactionValueStore(callback);
3100
3155
  if (Node.isPropertyAccessExpression(callee)) {
3101
3156
  /**
3102
3157
  * `q.forUpdate()`, `q.where(…)`: a query-builder chain hands the same rows on —
@@ -3144,6 +3199,14 @@ function createAnalyzer(app, stores, options = {}) {
3144
3199
  */
3145
3200
  const store = storeOfValue(initializer);
3146
3201
  if (store) symbols.set(nameNode.getText(), store);
3202
+ } else if (Node.isObjectBindingPattern(nameNode)) {
3203
+ const call = unwrapAwait(initializer);
3204
+ const callback = Node.isCallExpression(call) ? transactionCallbackOf(call) : null;
3205
+ if (!callback) return;
3206
+ for (const element of nameNode.getElements()) {
3207
+ const store = transactionValueStore(callback, element.getPropertyNameNode()?.getText() ?? element.getName());
3208
+ if (store) symbols.set(element.getName(), store);
3209
+ }
3147
3210
  }
3148
3211
  return;
3149
3212
  }
@@ -4194,8 +4257,15 @@ function isUnreadableWrite(call, symbols, imports, packageImports, body) {
4194
4257
  const nameNode = declaration.getNameNode();
4195
4258
  if (!Node.isIdentifier(nameNode) || nameNode.getText() !== root) continue;
4196
4259
  const initializer = declaration.getInitializer();
4197
- const origin = initializer ? rootSymbolOf(unwrapAwait(initializer)) : null;
4198
- if (origin && packageImports.has(origin)) return false;
4260
+ const initialized = initializer ? unwrapAwait(initializer) : null;
4261
+ /**
4262
+ * `await db.transaction(async (trx) => …)` is rooted at a package too, but its value
4263
+ * is what the CALLBACK returns — the application's. Only a call with no function
4264
+ * argument is a package's own object.
4265
+ */
4266
+ const handsBackACallback = initialized && Node.isCallExpression(initialized) && initialized.getArguments().some((a) => Node.isArrowFunction(a) || Node.isFunctionExpression(a));
4267
+ const origin = initialized ? rootSymbolOf(initialized) : null;
4268
+ if (origin && packageImports.has(origin) && !handsBackACallback) return false;
4199
4269
  }
4200
4270
  return true;
4201
4271
  }
@@ -5142,13 +5212,14 @@ const RULESET = "afp";
5142
5212
  * the guard's user, a service the container made — and a write nobody can type is
5143
5213
  * an unresolved call instead of silence. Two in 1.8.0: a method the model
5144
5214
  * declares names the store its caller writes, and a listener written inline on a
5145
- * string event is followed like a listener class.
5215
+ * string event is followed like a listener class. One in 1.9.0: the value of a
5216
+ * transaction callback is what its returns name.
5146
5217
  *
5147
5218
  * Without the bump, a baseline saved by the previous version compares cleanly
5148
5219
  * against this one and bills the tool's own improvement as work done. The guard
5149
5220
  * exists for exactly that, and only this constant arms it.
5150
5221
  */
5151
- const RULESET_VERSION = "1.8.0";
5222
+ const RULESET_VERSION = "1.9.0";
5152
5223
  function count(input, options = {}) {
5153
5224
  const warnings = [];
5154
5225
  const usage = usageOf(input);
@@ -5296,7 +5367,7 @@ function unreadableDeliveryWarnings(input) {
5296
5367
  if (blind.length === 0) return [];
5297
5368
  return [
5298
5369
  `${blind.length} transaction(s) deliver a value the analysis cannot read — a generated document, a call nobody followed — counted as 1 DET each, a FLOOR. This UNDERSTATES the output:`,
5299
- ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.delivered.opaqueFields.join(", ")}`),
5370
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.delivered.opaqueFields.join(", ")}`),
5300
5371
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5301
5372
  ];
5302
5373
  }
@@ -5312,7 +5383,7 @@ function unreadablePageWarnings(input) {
5312
5383
  for (const entry of input.entryPoints) {
5313
5384
  const behavior = input.behaviors.get(entry.id);
5314
5385
  if (!behavior || behavior.writes) continue;
5315
- for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.trigger} ${entry.signature}: ${store} leaves whole — ${reason}`);
5386
+ for (const [store, reason] of Object.entries(behavior.unreadablePages ?? {})) lines.push(` ${entry.identity}: ${store} leaves whole — ${reason}`);
5316
5387
  }
5317
5388
  if (lines.length === 0) return [];
5318
5389
  return [
@@ -5446,7 +5517,7 @@ function unreadableInputWarnings(input) {
5446
5517
  if (blind.length === 0) return [];
5447
5518
  return [
5448
5519
  `${blind.length} transaction(s) read the request without enumerating fields (\`all()\`, \`body()\`, \`except()\`), so their input DETs could not be counted and each sits at the floor of its band. This UNDERSTATES the total — the fix is a validator, not a configuration:`,
5449
- ...blind.slice(0, 10).map(({ entry }) => ` ${entry.trigger} ${entry.signature}`),
5520
+ ...blind.slice(0, 10).map(({ entry }) => ` ${entry.identity}`),
5450
5521
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5451
5522
  ];
5452
5523
  }
@@ -5467,7 +5538,7 @@ function unreadableOutputWarnings(input) {
5467
5538
  if (blind.length === 0) return [];
5468
5539
  return [
5469
5540
  `${blind.length} transaction(s) pass through a transformer that spreads something the analysis cannot read, counted as 1 DET each — a FLOOR. This UNDERSTATES the output; the fix is in the transformer (\`this.pick(...)\` or named keys), not a configuration:`,
5470
- ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.trigger} ${entry.signature}: ${behavior.opaqueOutputFields.join(", ")}`),
5541
+ ...blind.slice(0, 10).map(({ entry, behavior }) => ` ${entry.identity}: ${behavior.opaqueOutputFields.join(", ")}`),
5471
5542
  ...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
5472
5543
  ];
5473
5544
  }
@@ -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
2
  import { y as toPosix } from "./resolvers-CneCj3sT.js";
3
- import { n as analyze } from "./pipeline-DqPrDZfD.js";
3
+ import { n as analyze } from "./pipeline-4sZBYGKU.js";
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { existsSync } from "node:fs";
@@ -46,13 +46,14 @@ export declare const RULESET = "afp";
46
46
  * the guard's user, a service the container made — and a write nobody can type is
47
47
  * an unresolved call instead of silence. Two in 1.8.0: a method the model
48
48
  * declares names the store its caller writes, and a listener written inline on a
49
- * string event is followed like a listener class.
49
+ * string event is followed like a listener class. One in 1.9.0: the value of a
50
+ * transaction callback is what its returns name.
50
51
  *
51
52
  * Without the bump, a baseline saved by the previous version compares cleanly
52
53
  * against this one and bills the tool's own improvement as work done. The guard
53
54
  * exists for exactly that, and only this constant arms it.
54
55
  */
55
- export declare const RULESET_VERSION = "1.8.0";
56
+ export declare const RULESET_VERSION = "1.9.0";
56
57
  export type CountInput = {
57
58
  app: AppContext;
58
59
  stores: CollectedDataStore[];
package/build/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as CoverageTooLowError } from "../pipeline-DqPrDZfD.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-BuZNr-FE.js";
1
+ import { t as CoverageTooLowError } from "../pipeline-4sZBYGKU.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-b1tgLA1B.js";
3
3
  import path from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -1,2 +1,2 @@
1
- import { n as analyze, t as CoverageTooLowError } from "../pipeline-DqPrDZfD.js";
1
+ import { n as analyze, t as CoverageTooLowError } from "../pipeline-4sZBYGKU.js";
2
2
  export { CoverageTooLowError, analyze };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@filipebraida/adonis-function-points",
3
3
  "description": "Automated function point counting and code metrics for AdonisJS applications.",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "engines": {
6
6
  "node": ">=24.0.0"
7
7
  },