@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
package/CHANGELOG.md CHANGED
@@ -6,6 +6,161 @@ 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.6.0
10
+
11
+ **Rule set `afp@1.5.0`.** Six rules change what a number is made of, all of them found
12
+ by counting real applications function by function against what a certified counter
13
+ would write down — and every one was frozen in a fixture with a hand-written reference
14
+ **before** the code. A 0.5.0 baseline has to be recounted; on the three applications
15
+ this release was reviewed against the totals move −18, −8 and −37 FP, and the whole
16
+ of it is EOs losing DETs they never showed and one data function that was four.
17
+
18
+ ### Counting
19
+
20
+ - **An output's DETs are what leaves the boundary, not every column read.** A
21
+ transformer decides the output of the store it is FOR (`BaseTransformer<X>`, and the
22
+ resources of the transformers nested in it): the keys the reached method returns, a
23
+ nested transformer's keys once, `this.pick([...])` by name, `xs.map(...)` as a
24
+ repeating group. For every other store touched, what leaves is what the code shows:
25
+ rows whole (every column), the columns a `.select()` names, or **one derived scalar**
26
+ for `.count()` / `.exists()`. A relation preloaded through a covered store and read
27
+ no other way is covered too — it was loaded for the transformer. A spread the walker
28
+ cannot read counts 1 DET as a floor and is reported, like an open input object.
29
+ Before, a detail page through three transformers came out at 67 DET; a dashboard of
30
+ eight counters at 84.
31
+ - **A system timestamp is not a DET.** `autoCreate` / `autoUpdate` say the framework
32
+ stamps the column; the user neither supplies nor recognises it — the ground the key
33
+ was already excluded on. Excluded on the data function, on every output, and on a
34
+ transformer that re-emits it. A `dateTime` the user sets still counts. AFP §7.2 on
35
+ its letter would count both; the departure is now consistent, and counting-decisions
36
+ §6 says so.
37
+ - **A column declared `serializeAs: null` never leaves.** Lucid does not serialise it,
38
+ so it is not an output DET however the store leaves. It stays a DET of the data
39
+ function. Found as `User.password` on an activity log's output.
40
+ - **A detail the user only sees inside its master is a RET, not a data function.** A
41
+ `hasMany` / `hasOne` child that no application code addresses directly — only
42
+ `related()` / `preload()` from the parent — folds into the parent: one ILF with N RET,
43
+ the child's link to the parent excluded from the DETs, one FTR for a transaction
44
+ touching both. A child with a query of its own stays its own file (the Vazquez
45
+ benchmark depends on it); a child hanging off two parents stays apart and the report
46
+ says why. Cascade delete was measured and rejected as the signal: on one application
47
+ 11 of 13 cascades pointed at the tenant table. On the three applications the rule
48
+ folds four stores in all — the `Inpi*` mirror of an external registry becomes **one**
49
+ EIF with 4 RET, which is what the CPM says.
50
+ - **A data function is identified by its table**, as counting-decisions §5 always said.
51
+ Keyed by the class, renaming a model billed as a deletion plus an addition.
52
+ - **A token table is technical.** `password_reset_tokens`, `auth_access_tokens`,
53
+ `remember_me_tokens` are the machinery of authentication; `.*tokens?.*` joins the
54
+ naming list. And the list is now configurable in fact — `boundary.technicalPatterns`
55
+ replaces it, `DEFAULT_TECHNICAL_PATTERNS` is exported to start from — as §4 and the
56
+ filter's own comment had claimed since 0.1.0 while `counter.ts` passed nothing.
57
+
58
+ ### Configuration
59
+
60
+ - **`opaque.<Store.column | validator.field>`** replaces `overrides.<fn>.detFromSchema`
61
+ and `overrides.<fn>.opaqueReviewed`. A declaration about a DET the analysis cannot read
62
+ is about the column or the validator field — its ORIGIN — and applies to every
63
+ function that carries it: the ILF, the transaction that submits it, each screen that
64
+ shows it. Read from a real configuration, the old shape had the same mapping written
65
+ twice and a `GET` returning the same column still at 1 DET: the same column worth two
66
+ numbers in one count. Reviews are matched exactly; matched by bare name, reviewing
67
+ `Message.schema` reviewed every `schema` column of every store. A column may be keyed
68
+ by model or by table. The two old keys are gone from the type — an old configuration
69
+ fails to typecheck — and, because a configuration file is loaded without types,
70
+ `fp:count` still tells one that carries them that they had no effect.
71
+ `overrides.<fn>.det` / `.refs` remain per function.
72
+ - **`dataFunctions.grouping: 'usage' | 'none'`** replaces `retStrategy`. `none` is the
73
+ 0.5.0 behaviour — every table its own data function at RET 1 — for comparing with an
74
+ old count; it is not a preference. `retStrategy` is gone from the type, and `fp:count`
75
+ says so when an untyped configuration still carries it.
76
+ - **`boundary.technicalPatterns`**, see above.
77
+
78
+ ### New
79
+
80
+ - **The count names what it cannot decide.** Two transactions of the same type that
81
+ reach the same stores, emit the same DETs and walk the same bodies below the
82
+ controller are reported as look-alikes with the FP at stake — the CPM counts
83
+ identical processing logic once, and `boundary.ignoreEntryPoints` records the
84
+ decision. An EIF only a seeder writes is reported too: code data the team maintains
85
+ is not counted, a mirror of another system's data is a legitimate EIF, and the code
86
+ cannot tell which. Neither moves a number (counting-decisions §11).
87
+ - **`diff.preset: 'sisp'`** prices change by the Roteiro de Métricas de Software do SISP
88
+ v3.0 (Portaria SGD/MGI nº 3656/2026), §7.3 — inclusão 1,00, alteração × FI 0,63 (the
89
+ contractor maintains its own work; 0,84 otherwise, via `factors`), exclusão 0,50 —
90
+ instead of AEP. `fp:diff` prints which preset produced the billable total. Read from
91
+ the guide's PDF: a first draft of this preset said 0,50 / 0,30 from memory, and v2.0
92
+ (2012) priced exclusion at 0,40 — a contract binds to a revision, so check yours.
93
+ - **`ignoreCalls({ name, methods | matching })`** builds a "this reaches no data"
94
+ strategy without the ceremony a real configuration had to carry — a helper to read
95
+ the method name off a ts-morph node, a `resolve` that returns nothing, one comparison.
96
+ The strategy is still named, and its volume is still reported.
97
+ - The configuration stub no longer echoes defaults.
98
+
99
+ ### Documented
100
+
101
+ - counting-decisions §6 now describes what the code does for output DETs, row by row,
102
+ and records the two refinements the recounts forced; §4 says plainly that only the
103
+ naming mechanism of the technical filter exists, and why the lookup-structure rule
104
+ was rejected; §9 gains "Declared by origin, not by function"; §10 is the master-detail
105
+ rule, with the cascade measurement that rejected the structural signal; §11 is what
106
+ the count reports because it cannot decide.
107
+ - Five reference fixtures were written before their rules: `transformed_output` (59 FP,
108
+ eight output shapes), `system_timestamps` (18), `mestre_detalhe` (41; 51 with
109
+ grouping off), and additions to `edges_boundary` and `open_object`.
110
+
111
+ ## 0.5.0
112
+
113
+ **Rule set `afp@1.4.0`.** Two classification defects are fixed and both move numbers,
114
+ so a 0.4.0 baseline has to be recounted.
115
+
116
+ Found by auditing a production count function by function against the code, rather than
117
+ by reading the report's warnings — which is where the previous rounds had been looking.
118
+
119
+ ### Fixed
120
+
121
+ - **Maintenance was decided per REQUEST instead of per store.** `behavior.writes`
122
+ decides EI against EO and was also read as "this store is maintained", so every store
123
+ a writing transaction touched became an ILF. A reference table merely READ by a route
124
+ that writes something else counted as maintained. On a production application this
125
+ left exactly one EIF in the whole count, which should have been the signal.
126
+ - **Seeders, tests and factories counted as maintenance.** The project-wide pass read a
127
+ seeder's inserts as the application maintaining a table, so reference data only the
128
+ seed populates came out as an ILF — which the CPM does not allow. The filter on scan
129
+ roots drops `tests/` and `database/` only at the ROOT, and a domain-module layout puts
130
+ both inside `app/`. It now applies at any depth.
131
+ - **The override warning counted floors that were already answered**, said "one schema"
132
+ whatever it was given, and therefore fired on a configuration that was complete.
133
+ - **`opaqueReviewed` matching nothing was silent.** `detFromSchema` already warns when
134
+ it names a schema that is not declared; a review naming a field that does not exist
135
+ reviewed nothing while the warning kept firing, which reads as the tool ignoring the
136
+ configuration.
137
+ - **A review was invisible in `fp:explain`.** Its reason appeared nowhere, which defeats
138
+ requiring one. Reviewed floors are now marked `(opaque, reviewed)` and the reason is
139
+ printed — without being counted in the "Declared by override" share, since a review
140
+ declares no number.
141
+
142
+ ### Documented
143
+
144
+ - **In CI, prefer the standalone binary.** `node ace` validates `start/env.ts` before
145
+ running any command, so `node ace fp:count` fails on a missing environment variable
146
+ that has nothing to do with counting — measured on a production application, it
147
+ stopped at `Missing environment variable "AUTHZ_STORE"` and never reached the
148
+ command. The `fp:*` commands declare `startApp: false`, which is not enough. The
149
+ README said the two front-ends were interchangeable; for a pipeline that only checks
150
+ out code, they are not.
151
+
152
+ ### New
153
+
154
+ - **`CallResolver.technicalWrite()`** declares that a write is not what the transaction
155
+ is for. §6.5.3 reads any write as an EI, which misreads a screen that records the
156
+ visit; the CPM asks about primary intent. The fact is declared about the CALL, so a
157
+ bookkeeping helper called from several screens is declared once. It does not hide the
158
+ write: the store stays an ILF and stays an FTR.
159
+ - **`boundary.business` says when it contradicts the code.** It accepted without comment
160
+ a table nothing in the application writes — which is how two read-only lookup tables
161
+ were declared as business data on the belief they had a CRUD, when the routes were
162
+ `.only(['index', 'show'])`. The declaration is still honoured; the fact is reported.
163
+
9
164
  ## 0.4.0
10
165
 
11
166
  **Rule set `afp@1.3.0`.** Conditional validator groups now count, and a nested
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.3.0
16
+ Ruleset: afp@1.5.0
17
17
 
18
18
  type n FP
19
19
  ILF 2 14
@@ -34,18 +34,6 @@ GET /presenca EO 13 3 5
34
34
  GET /presenca/relatorio EO 13 3 5
35
35
  ```
36
36
 
37
- ## Why
38
-
39
- Software factories bill by function point, and the count is manual, slow, and
40
- varies from counter to counter. Commercial automated counters exist for
41
- enterprise legacy, but **no modern framework has one** — not Laravel, not Rails,
42
- not AdonisJS. What those ecosystems do have (`rails stats`, `laravel-stats`,
43
- `adonisjs-stats`) counts classes and lines, which is a different thing.
44
-
45
- This package implements the OMG **Automated Function Points** specification,
46
- which defines how to automate IFPUG CPM by replacing the subjective judgements
47
- with deterministic rules.
48
-
49
37
  ## Install
50
38
 
51
39
  ```bash
@@ -53,80 +41,17 @@ npm i @filipebraida/adonis-function-points
53
41
  node ace configure @filipebraida/adonis-function-points
54
42
  ```
55
43
 
56
- Requires **AdonisJS 7** and **Lucid 22** (see [Support](#support)).
57
-
58
- ### Or run it without installing
59
-
60
- For CI, or a one-off count on a project you do not want to touch:
44
+ Requires **AdonisJS 7** and **Lucid 22** (see [Support](#support)). In CI, or
45
+ for a one-off count on a project you do not want to touch, the standalone
46
+ binary needs no install, no `.env` and no database:
61
47
 
62
48
  ```bash
63
49
  npx @filipebraida/adonis-function-points count --root ./my-app
64
50
  ```
65
51
 
66
- Nothing is booted either way — the engine only reads files — so a standalone
67
- run needs no `.env`, no database, and no install inside the analysed project.
68
- The standalone binary **does not replace installing**: a project that installs
69
- the package keeps the `node ace fp:*` commands, and both front-ends call the
70
- same code, so they cannot disagree about a number.
71
-
72
- ```
73
- adonis-function-points <command> [options]
74
-
75
- count count the unadjusted function points
76
- inventory the raw facts: stores, routes, tracing coverage
77
- explain <name> why one function was counted that way
78
- diff <previous.json> additions / modifications / deletions, and billable FP
79
- calibrate <samples.csv> correction factors against a manual count
80
-
81
- --root <path> application to analyse (default: the current directory)
82
- --out <path> write the result as JSON to this path
83
- --json print JSON instead of a table
84
- --min-coverage <0..1> fail below this tracing coverage
85
- ```
86
-
87
- Both front-ends exit non-zero when the count cannot be produced — coverage
88
- below the minimum, an unreadable configuration, a saved count from a different
89
- ruleset — so a CI job fails instead of publishing a number nobody can defend.
90
-
91
- #### In CI
92
-
93
- Every count records what it counted, so the artefact stands on its own once it
94
- leaves the pipeline:
95
-
96
- ```json
97
- "source": {
98
- "app": "shop",
99
- "revision": "adef4ee3…",
100
- "branch": "main",
101
- "dirty": false,
102
- "countedAt": "2026-09-24T17:40:11.000Z",
103
- "config": "/app/config/function_points.ts"
104
- }
105
- ```
106
-
107
- `dirty` is the field that matters in billing: a count taken over uncommitted
108
- changes cannot be reproduced from any revision, and whoever receives the
109
- invoice is entitled to know that. `app` is the manifest name, never an absolute
110
- path — a path would say where your machine keeps its files and travel with
111
- every count you send anywhere.
112
-
113
- `fp:diff` refuses two counts of different applications, the same way it refuses
114
- two different rulesets, and warns when either side is dirty or when both are
115
- the same revision.
116
-
117
- Counting an older revision needs no checkout of your working tree and nothing
118
- installed in it, so a pull request is two counts and a comparison:
119
-
120
- ```yaml
121
- - run: git worktree add ../base ${{ github.event.pull_request.base.sha }}
122
- - run: npx @filipebraida/adonis-function-points count --root ../base --out base.json
123
- - run: npx @filipebraida/adonis-function-points count --out head.json
124
- - run: npx @filipebraida/adonis-function-points diff base.json head.json
125
- ```
126
-
127
- The package does not deliver the result anywhere — an artifact, a ledger
128
- branch, a billing endpoint and a PR comment are all yours to choose. What it
129
- owes you is a number that is still defensible wherever it lands.
52
+ Both front-ends call the same code and cannot disagree about a number. Which
53
+ one to use where, what a saved count records, and how a pull request becomes
54
+ two counts and a comparison: [`docs/ci.md`](docs/ci.md).
130
55
 
131
56
  ## Commands
132
57
 
@@ -144,73 +69,25 @@ saved count against the current state of the application. It deliberately does
144
69
  **not** take a git ref: booting an older checkout, with possibly different
145
70
  dependencies, is a problem not worth solving.
146
71
 
147
- ### How change is priced
148
-
149
- AEP §6.5 gives explicit anchors for added (1) and deleted (0.4). For a **modified**
150
- function it grades the factor from 0.25 to 1.75 through Effort Complexity
151
- variation, which needs cyclomatic complexity this package does not measure — so it
152
- defaults to 1, which overestimates, and every diff says so with the amount at
153
- stake.
154
-
155
- What the default leaves on the table is a distinction the tool already measures:
156
-
157
- ```
158
- changed 87 functions 378 FP × 1
159
- type 4 functions 23 FP
160
- size 38 functions 204 FP
161
- implementation 45 functions 151 FP
162
- ```
163
-
164
- `implementation` means same type, same DET, same FTR, different body — a refactor.
165
- On a real pair of releases that was 151 of 378 FP billed as change. Pricing it at
166
- full functional value is not defensible, and pricing it at a number this package
167
- invented would be worse, so the number comes from the contract:
168
-
169
- ```ts
170
- export default defineConfig({
171
- diff: {
172
- reasonFactors: { implementation: 0.25 },
173
- },
174
- })
175
72
  ```
73
+ adonis-function-points <command> [options]
176
74
 
177
- ### `fp:metrics` — the counterweight
178
-
179
- If function points pay, the team optimises function points: more models, more
180
- endpoints, less reuse. So density and coupling are reported from the **same**
181
- inventory, and this command exists to put them on the same page as the number.
75
+ count count the unadjusted function points
76
+ inventory the raw facts: stores, routes, tracing coverage
77
+ explain <name> why one function was counted that way
78
+ metrics density, coupling and conformance, from the same run
79
+ diff <previous.json> additions / modifications / deletions, and billable FP
80
+ calibrate <samples.csv> correction factors against a manual count
182
81
 
183
- ```
184
- Density
185
- FP per data store: 27.0
186
- transactions per data store: 5.4
187
-
188
- Conformance
189
- inputs with a validator 100.0% (30/30)
190
- entry points with a handler 100.0% (163/163)
191
- data stores reached 96.7% (29/30)
192
- tracing coverage 95.1% (15 unresolved calls)
193
-
194
- module FP trans stores I depends on
195
- pedidos 211 33 8 0.60 inventores, tecnologias, users
196
- portal 69 15 0 1.00 contato, documentos, inventores, ...
197
- inpi 70 11 6 0.00
198
-
199
- Mutual dependencies (cycle candidates)
200
- inventores <-> tecnologias
82
+ --root <path> application to analyse (default: the current directory)
83
+ --out <path> write the result as JSON to this path
84
+ --json print JSON instead of a table
85
+ --min-coverage <0..1> fail below this tracing coverage
201
86
  ```
202
87
 
203
- The denominator of the first line is the transactions that **take** input, not
204
- every write. Measured over every write it read 39% on a healthy application, which
205
- invites the conclusion that 61% of its writes are unvalidated — and they are not:
206
- most are workflow triggers (`POST /orders/:id/submit`) that carry nothing beyond
207
- the route parameter. A metric that makes the reader draw a false conclusion is
208
- worse than no metric.
209
-
210
- `I` is Martin's instability, `Ce / (Ca + Ce)`: 0 means everyone depends on it and
211
- it depends on nobody, 1 means the reverse. A module at 0 that changes often is
212
- where change hurts. Cycles are **reported, not scored** — what to do about one is
213
- the team's decision, and a number would hide it.
88
+ Every command exits non-zero when the count cannot be produced — coverage
89
+ below the minimum, an unreadable configuration, a saved count from a different
90
+ rule set — so a CI job fails instead of publishing a number nobody can defend.
214
91
 
215
92
  ### `fp:explain` — the number has to be defensible
216
93
 
@@ -236,72 +113,22 @@ Path walked:
236
113
  If function points get invoiced, someone will dispute a number — and a number
237
114
  without provenance is indefensible.
238
115
 
239
- ## Benchmark
116
+ ### `fp:metrics` — the counterweight
240
117
 
241
- The only reference in this project not produced by its own authors is the case
242
- study published in **Vazquez, Simões & Albert (2011)**, the same one used by the
243
- COPPE/UFRJ dissertation on the _Ligeiro_ tool (Pinel, 2012).
244
-
245
- The fixture and the reference count were frozen in their own commit **before**
246
- the counter was ever run against them, with the transcription choices written
247
- down first. Without that the independence would be illusory.
248
-
249
- | | total | vs reference |
250
- | ----------------------------------- | --------- | ------------ |
251
- | **Vazquez et al. (2011), manual** | **46 FP** | — |
252
- | **this package** | **46 FP** | **0%** |
253
- | Ligeiro, automated (Pinel 2012) | 52 FP | +13% |
254
- | Ligeiro, manual under its own rules | 43 FP | −6.5% |
255
-
256
- Eight of the ten functions match exactly. The two that do not were **predicted
257
- in writing before the run**, and come from the standard rather than from
258
- defects:
259
-
260
- - **+1** `Consulta Apontamento Diário` is an EQ in the reference; AFP §6.5.3
261
- requires collapsing EQ into EO, and an EO weighs more in the same band.
262
- - **−1** `Apontamento c/ Justificativa`: the IFPUG manual counts 1 DET for the
263
- user message, AFP does not.
264
-
265
- They cancel out, which is exactly why the total is reported alongside the
266
- function-by-function agreement rather than on its own.
267
-
268
- Reproduce it with `npm test` — the benchmark is
269
- `tests/acceptance/vazquez.spec.ts`, and the reference is
270
- [`tests/fixtures/apps/vazquez/REFERENCE.md`](tests/fixtures/apps/vazquez/REFERENCE.md).
271
-
272
- ## Principles
273
-
274
- **Traceability.** Every counted function says where it came from: file, line,
275
- rule applied, origin of each DET and each FTR, and the path walked through the
276
- call graph. The ruleset is versioned and printed in every report — two counts
277
- are only comparable if the rules did not change in between.
278
-
279
- **Say "I don't know" rather than be wrong in silence.** A call the tracer cannot
280
- follow enters the coverage metric. If coverage falls below the configured
281
- threshold, the analysis **fails** instead of emitting a number that looks right.
282
- This is not a preference; AFP §6.5.3 requires it:
283
-
284
- > "If the transaction execution depends on code that is unknown or unavailable
285
- > to the automated tool, the code end point shall be cataloged and listed in the
286
- > generated report in order to detect and quantify the missing patterns and
287
- > libraries."
288
-
289
- **Shape must not change the count.** The same logical application written in
290
- different ways — flat MVC or module-per-domain, fat controller or action object,
291
- generated artefacts or none — must produce an identical number. That is the
292
- project's golden invariant, and it is a test
293
- (`tests/acceptance/golden_invariant.spec.ts`) that was written before the first
294
- collector.
295
-
296
- **Extensibility as a requirement.** AdonisJS imposes no code organisation — fat
297
- controller, action object, static service, injected service, module function,
298
- job. Tracing strategies are registrable, so a project with its own convention
299
- registers it (see [Custom code pattern](#custom-code-pattern)).
300
-
301
- **Function points are not the only number on the dashboard.** If function points
302
- pay, the team optimises function points: more models, more endpoints, less
303
- reuse. Coupling, instability and density come free from the same inventory, and
304
- are the counterweight.
118
+ If function points pay, the team optimises function points: more models, more
119
+ endpoints, less reuse. Density, coupling, instability and conformance come free
120
+ from the same inventory and are reported beside the number — what each metric
121
+ means and what it deliberately does not measure: [`docs/metrics.md`](docs/metrics.md).
122
+
123
+ ### `fp:diff` — what change is worth
124
+
125
+ Additions, modifications and deletions between two saved counts, priced by the
126
+ factors the contract names: AEP by default, or `diff: { preset: 'sisp' }` for the
127
+ Roteiro de Métricas do SISP v3.0 (inclusão 1,00, alteração × FI 0,63, exclusão
128
+ 0,50; `factors: { changed: 0.84 }` when the contractor did not develop the function). A modification is split by **why** it changed —
129
+ type, size, or implementation only — so a refactor is visible before it is
130
+ billed at full value. The reasoning, and what the default leaves on the table:
131
+ [counting-decisions §5](docs/design/counting-decisions.md#5-identity-of-a-function-across-versions-fpdiff).
305
132
 
306
133
  ## Configuration
307
134
 
@@ -321,10 +148,11 @@ export default defineConfig({
321
148
  infrastructure: ['access_tokens', 'audits'], // excluded, with the reason in the report
322
149
  externallyMaintained: ['erp_customers'], // counted as EIF instead of ILF
323
150
  business: ['chat_sessions'], // the AFP naming filter caught it by accident
151
+ // technicalPatterns: [...DEFAULT_TECHNICAL_PATTERNS], // replaces the filter's naming list
324
152
  ignoreEntryPoints: ['prometheus.metrics'],
325
153
  },
326
154
 
327
- retStrategy: 'constant', // or 'composition'
155
+ dataFunctions: { grouping: 'usage' }, // 'none' keeps every table its own data function
328
156
  maxDepth: 3, // how far to follow the call graph
329
157
  messageDet: 0, // 1 restores the IFPUG confirmation-message DET
330
158
  minCoverage: 0.85, // below this, the analysis fails
@@ -380,22 +208,25 @@ over a rate limiter or an attachment variant is a real example. Without a way
380
208
  to say so, such a call stays unresolved and drags the coverage gate down.
381
209
 
382
210
  ```ts
383
- const limiterIsDataFree: CallResolver = {
384
- name: 'login-limiter',
385
- order: 1,
386
- resolve: () => [],
387
- ignores(call) {
388
- // true means: this is mine, and it touches no data store
389
- return call.getExpression().getText().startsWith('this.loginLimiter.')
211
+ import { ignoreCalls } from '@filipebraida/adonis-function-points'
212
+
213
+ export default defineConfig({
214
+ resolvers: {
215
+ call: [
216
+ ignoreCalls({ name: 'login-limiter', matching: /^this\.loginLimiter\./ }),
217
+ ignoreCalls({ name: 'attachment-variants', methods: ['getUrl', 'getVariant'] }),
218
+ ],
390
219
  },
391
- }
220
+ })
392
221
  ```
393
222
 
394
- `ignores` is asked before `resolve`, in the same order, so a later and more
395
- generic strategy cannot follow the call into a body it has no business reading.
396
- It is deliberately more expensive than a list of method names to silence: the
397
- volume still appears in the confidence block of `fp:count`, because a silent
398
- drop is the worst defect this package can have — whoever writes it.
223
+ Or, for a shape the factory does not cover, a strategy with `ignores(call)`
224
+ returning `true` — "this is mine, and it touches no data store". `ignores` is
225
+ asked before `resolve`, in the same order, so a later and more generic strategy
226
+ cannot follow the call into a body it has no business reading. It stays a
227
+ **named** strategy on purpose: the volume it declared data-free still appears in
228
+ the confidence block of `fp:count`, because a silent drop is the worst defect
229
+ this package can have — whoever writes it.
399
230
 
400
231
  ## Support
401
232
 
@@ -415,9 +246,12 @@ Inherited from the AFP standard itself, not from this implementation:
415
246
  - **EQ is collapsed into EO.** Telling an inquiry from an output requires
416
247
  knowing whether there is derived data or calculation, which static analysis
417
248
  cannot see. AFP mandates the collapse.
418
- - **RET is approximated.** What a user recognises as a logical subgroup is not
419
- derivable from code. The default pins it at 1; `composition` derives it from
420
- composition relations.
249
+ - **RET comes from usage, not from the user's view.** A `hasMany`/`hasOne`
250
+ child that no application code addresses directly folds into its parent as a
251
+ RET; one that has its own queries stays its own data function. That is the
252
+ only signal static analysis has, and it is conservative: it groups only when
253
+ the code cannot see the child apart from the parent. A child hanging off two
254
+ parents stays apart and is reported.
421
255
  - **Confirmation and error messages** count 1 DET in a manual count and are
422
256
  invisible here — a known systematic divergence of −1 DET per transaction.
423
257
  `messageDet: 1` restores it.
@@ -425,44 +259,42 @@ Inherited from the AFP standard itself, not from this implementation:
425
259
  judgement. AFP fixes VAF = 1, and the unadjusted count is what public
426
260
  contracts in Brazil use anyway.
427
261
  - **The modification factor in `fp:diff` is 1.** AEP grades it from 0.25 to
428
- 1.75 using Effort Complexity, which requires cyclomatic complexity. A flat 1
429
- does not discriminate — it prices a one-line fix and a rewrite the same — and
430
- the report says so.
431
-
432
- What it does discriminate is **why** a function changed, which is usually the
433
- larger question:
434
-
435
- ```
436
- changed 74 functions 318 FP × 1
437
- type 1 functions 3 FP reclassified, e.g. EO -> EI
438
- size 28 functions 141 FP DET or FTR moved
439
- implementation 45 functions 174 FP same size, different code
440
- ```
441
-
442
- On a real month of work that is 44% of the invoice coming from refactoring.
443
- Whether that should be billed at full value is a contract decision, not a
444
- counting one — but it has to be visible before anyone can make it.
445
-
262
+ 1.75 through Effort Complexity, which needs cyclomatic complexity this package
263
+ does not measure. A flat 1 prices a one-line fix and a rewrite the same, and
264
+ the report says so with the amount at stake. What it does discriminate is
265
+ **why** a function changed — type, size, or implementation only — and
266
+ `diff.reasonFactors` prices a refactor by the contract rather than by a number
267
+ the tool invented. See counting-decisions §5.
446
268
  - **Schema-driven applications undercount their input.** When the fields a user
447
269
  fills live in a JSON column whose schema is stored in the database, there is
448
- nothing for static analysis to read: each opaque column counts as 1 DET.
449
- Measured on a production application, the effect is about 2% of the total —
450
- data functions are unaffected, and only the form-submission transaction loses
451
- complexity.
452
-
453
- `fp:count` names every opaque column a transaction reaches, so the limitation
454
- is visible where you read the number rather than only in a design document.
455
- The way out is to declare the number rather than let the tool guess it:
456
- `overrides: { 'POST /petitions': { det: 42, reason: '…' } }`. The reason is
457
- required by the type, `fp:explain` prints it beside the number, and `fp:count`
458
- reports what share of the total was declared — because an override is right
459
- where static analysis is blind and poison as a habit. See
460
- counting-decisions §8.
270
+ nothing for static analysis to read: the column counts as 1 DET — a floor,
271
+ and `fp:count` names it on every run. The way out is to declare where the
272
+ fields live, by origin, and the declaration reaches every function that
273
+ carries the column:
274
+
275
+ ```ts
276
+ opaque: {
277
+ 'Survey.answers': { schemas: 'surveySchema', reason: 'the form is a JSON Schema in the seed' },
278
+ }
279
+ ```
280
+
281
+ The reason is required, `fp:explain` prints it beside the number, and
282
+ `fp:count` reports what share of the total came from a declaration.
283
+ `overrides.<fn>.det` remains for a schema that lives only in the database.
284
+ See counting-decisions §8.
461
285
 
462
286
  - **Only HTTP routes are collected as entry points.** An ace command that
463
287
  imports a spreadsheet and a scheduled job are transactional functions under
464
288
  IFPUG; they are out of v1.
465
289
 
290
+ ## Benchmark
291
+
292
+ The only reference in this project not produced by its own authors is the case
293
+ study in **Vazquez, Simões & Albert (2011)**: 46 FP by hand, **46 FP** here, 8 of
294
+ 10 functions exact and the two that differ predicted in writing before the run.
295
+ How the fixture was frozen, what Ligeiro got, and why the total is more
296
+ defensible than any single function: [`docs/benchmark.md`](docs/benchmark.md).
297
+
466
298
  ## References
467
299
 
468
300
  - **OMG Automated Function Points (AFP) 1.0** — ISO/IEC 19515:2019. The
@@ -484,44 +316,32 @@ Inherited from the AFP standard itself, not from this implementation:
484
316
 
485
317
  The reasoning behind the count lives with the code:
486
318
 
487
- - [`docs/design/architecture.md`](docs/design/architecture.md) — the thesis, the
488
- layers, and what is discovered instead of configured
319
+ - [`docs/design/architecture.md`](docs/design/architecture.md) — why the package
320
+ exists, its principles, the layers, and what is discovered instead of configured
489
321
  - [`docs/design/counting-decisions.md`](docs/design/counting-decisions.md) —
490
322
  each edge case, with the AFP rule that settles it
491
323
  - [`docs/design/resolvers.md`](docs/design/resolvers.md) — the catalogue of code
492
324
  patterns and how each is followed
325
+ - [`docs/benchmark.md`](docs/benchmark.md), [`docs/ci.md`](docs/ci.md),
326
+ [`docs/metrics.md`](docs/metrics.md)
493
327
 
494
328
  Three further documents are kept as a **dated record** of how the design was
495
329
  arrived at, in Portuguese, and are not a reference for current behaviour:
496
- [`implementation-plan.md`](docs/design/implementation-plan.md) (built phase by
497
- phase, and what each phase found),
498
- [`adonisjs-variation.md`](docs/research/adonisjs-variation.md) (what varies
499
- between real AdonisJS applications) and
500
- [`external-validation.md`](docs/research/external-validation.md) (the thesis
501
- tested outside the sample that produced it).
330
+ [`implementation-plan.md`](docs/design/implementation-plan.md),
331
+ [`adonisjs-variation.md`](docs/research/adonisjs-variation.md) and
332
+ [`external-validation.md`](docs/research/external-validation.md).
502
333
 
503
334
  ## Contributing
504
335
 
505
336
  ```bash
506
337
  pnpm install
507
- pnpm test # lint + 242 tests, from source
508
- pnpm run typecheck
509
- pnpm run compile && pnpm run test:package # the packed tarball, installed and used
338
+ pnpm test # lint + the suite, from source
339
+ pnpm run compile && pnpm run test:package # the packed tarball, installed and used
510
340
  ```
511
341
 
512
- `test:package` is separate on purpose: the suite runs from source through
513
- ts-exec and never loads `build/`, which is the only thing a user gets. A
514
- release once had every `exports` path pointing at a file the build did not
515
- emit, with the whole suite green.
516
-
517
- Two house rules worth knowing before opening a PR:
518
-
519
- 1. **Example first.** A fixture with a known answer comes before the code. A
520
- fixture written after the code tests what the code does, not what it should
521
- do.
522
- 2. **A silent drop is the worst possible defect.** Anything the tracer cannot
523
- follow must land in `unresolved` with the _right_ reason, never be quietly
524
- treated as a read.
342
+ Two house rules: a fixture with a known answer comes **before** the code, and a
343
+ silent drop is the worst possible defect. The rest is in
344
+ [`CONTRIBUTING.md`](CONTRIBUTING.md).
525
345
 
526
346
  ## License
527
347