@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
package/CHANGELOG.md CHANGED
@@ -6,6 +6,108 @@ 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
+
9
111
  ## 0.5.0
10
112
 
11
113
  **Rule set `afp@1.4.0`.** Two classification defects are fixed and both move numbers,
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.4.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,91 +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
- The engine itself boots nothing — it only reads files — and the standalone binary
67
- needs no `.env`, no database, and no install inside the analysed project. The
68
- `fp:*` commands declare `startApp: false` for the same reason.
69
-
70
- **In CI, prefer the standalone binary.** Not for convenience: `node ace` validates
71
- `start/env.ts` before it runs any command, so `node ace fp:count` fails on a
72
- missing environment variable that has nothing to do with counting. Measured on a
73
- production application, it stopped at `Missing environment variable "AUTHZ_STORE"`
74
- and never reached the command. A pipeline that only checks out the code has no
75
- secrets, and does not need them to count.
76
-
77
- The standalone binary **does not replace installing**: a project that installs the
78
- package keeps the `node ace fp:*` commands — which is the right front-end at a
79
- developer's terminal, where the `.env` is already there — and both call the same
80
- code, so they cannot disagree about a number.
81
-
82
- ```
83
- adonis-function-points <command> [options]
84
-
85
- count count the unadjusted function points
86
- inventory the raw facts: stores, routes, tracing coverage
87
- explain <name> why one function was counted that way
88
- metrics density, coupling and conformance, from the same run
89
- diff <previous.json> additions / modifications / deletions, and billable FP
90
- calibrate <samples.csv> correction factors against a manual count
91
-
92
- --root <path> application to analyse (default: the current directory)
93
- --out <path> write the result as JSON to this path
94
- --json print JSON instead of a table
95
- --min-coverage <0..1> fail below this tracing coverage
96
- ```
97
-
98
- Both front-ends exit non-zero when the count cannot be produced — coverage
99
- below the minimum, an unreadable configuration, a saved count from a different
100
- ruleset — so a CI job fails instead of publishing a number nobody can defend.
101
-
102
- #### In CI
103
-
104
- Every count records what it counted, so the artefact stands on its own once it
105
- leaves the pipeline:
106
-
107
- ```json
108
- "source": {
109
- "app": "shop",
110
- "revision": "adef4ee3…",
111
- "branch": "main",
112
- "dirty": false,
113
- "countedAt": "2026-09-24T17:40:11.000Z",
114
- "config": "/app/config/function_points.ts"
115
- }
116
- ```
117
-
118
- `dirty` is the field that matters in billing: a count taken over uncommitted
119
- changes cannot be reproduced from any revision, and whoever receives the
120
- invoice is entitled to know that. `app` is the manifest name, never an absolute
121
- path — a path would say where your machine keeps its files and travel with
122
- every count you send anywhere.
123
-
124
- `fp:diff` refuses two counts of different applications, the same way it refuses
125
- two different rulesets, and warns when either side is dirty or when both are
126
- the same revision.
127
-
128
- Counting an older revision needs no checkout of your working tree and nothing
129
- installed in it, so a pull request is two counts and a comparison:
130
-
131
- ```yaml
132
- - run: git worktree add ../base ${{ github.event.pull_request.base.sha }}
133
- - run: npx @filipebraida/adonis-function-points count --root ../base --out base.json
134
- - run: npx @filipebraida/adonis-function-points count --out head.json
135
- - run: npx @filipebraida/adonis-function-points diff base.json head.json
136
- ```
137
-
138
- The package does not deliver the result anywhere — an artifact, a ledger
139
- branch, a billing endpoint and a PR comment are all yours to choose. What it
140
- 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).
141
55
 
142
56
  ## Commands
143
57
 
@@ -155,73 +69,25 @@ saved count against the current state of the application. It deliberately does
155
69
  **not** take a git ref: booting an older checkout, with possibly different
156
70
  dependencies, is a problem not worth solving.
157
71
 
158
- ### How change is priced
159
-
160
- AEP §6.5 gives explicit anchors for added (1) and deleted (0.4). For a **modified**
161
- function it grades the factor from 0.25 to 1.75 through Effort Complexity
162
- variation, which needs cyclomatic complexity this package does not measure — so it
163
- defaults to 1, which overestimates, and every diff says so with the amount at
164
- stake.
165
-
166
- What the default leaves on the table is a distinction the tool already measures:
167
-
168
- ```
169
- changed 87 functions 378 FP × 1
170
- type 4 functions 23 FP
171
- size 38 functions 204 FP
172
- implementation 45 functions 151 FP
173
- ```
174
-
175
- `implementation` means same type, same DET, same FTR, different body — a refactor.
176
- On a real pair of releases that was 151 of 378 FP billed as change. Pricing it at
177
- full functional value is not defensible, and pricing it at a number this package
178
- invented would be worse, so the number comes from the contract:
179
-
180
- ```ts
181
- export default defineConfig({
182
- diff: {
183
- reasonFactors: { implementation: 0.25 },
184
- },
185
- })
186
72
  ```
73
+ adonis-function-points <command> [options]
187
74
 
188
- ### `fp:metrics` — the counterweight
189
-
190
- If function points pay, the team optimises function points: more models, more
191
- endpoints, less reuse. So density and coupling are reported from the **same**
192
- 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
193
81
 
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
194
86
  ```
195
- Density
196
- FP per data store: 27.0
197
- transactions per data store: 5.4
198
-
199
- Conformance
200
- inputs with a validator 100.0% (30/30)
201
- entry points with a handler 100.0% (163/163)
202
- data stores reached 96.7% (29/30)
203
- tracing coverage 95.1% (15 unresolved calls)
204
-
205
- module FP trans stores I depends on
206
- pedidos 211 33 8 0.60 inventores, tecnologias, users
207
- portal 69 15 0 1.00 contato, documentos, inventores, ...
208
- inpi 70 11 6 0.00
209
-
210
- Mutual dependencies (cycle candidates)
211
- inventores <-> tecnologias
212
- ```
213
-
214
- The denominator of the first line is the transactions that **take** input, not
215
- every write. Measured over every write it read 39% on a healthy application, which
216
- invites the conclusion that 61% of its writes are unvalidated — and they are not:
217
- most are workflow triggers (`POST /orders/:id/submit`) that carry nothing beyond
218
- the route parameter. A metric that makes the reader draw a false conclusion is
219
- worse than no metric.
220
87
 
221
- `I` is Martin's instability, `Ce / (Ca + Ce)`: 0 means everyone depends on it and
222
- it depends on nobody, 1 means the reverse. A module at 0 that changes often is
223
- where change hurts. Cycles are **reported, not scored** — what to do about one is
224
- 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.
225
91
 
226
92
  ### `fp:explain` — the number has to be defensible
227
93
 
@@ -247,72 +113,22 @@ Path walked:
247
113
  If function points get invoiced, someone will dispute a number — and a number
248
114
  without provenance is indefensible.
249
115
 
250
- ## Benchmark
116
+ ### `fp:metrics` — the counterweight
251
117
 
252
- The only reference in this project not produced by its own authors is the case
253
- study published in **Vazquez, Simões & Albert (2011)**, the same one used by the
254
- COPPE/UFRJ dissertation on the _Ligeiro_ tool (Pinel, 2012).
255
-
256
- The fixture and the reference count were frozen in their own commit **before**
257
- the counter was ever run against them, with the transcription choices written
258
- down first. Without that the independence would be illusory.
259
-
260
- | | total | vs reference |
261
- | ----------------------------------- | --------- | ------------ |
262
- | **Vazquez et al. (2011), manual** | **46 FP** | — |
263
- | **this package** | **46 FP** | **0%** |
264
- | Ligeiro, automated (Pinel 2012) | 52 FP | +13% |
265
- | Ligeiro, manual under its own rules | 43 FP | −6.5% |
266
-
267
- Eight of the ten functions match exactly. The two that do not were **predicted
268
- in writing before the run**, and come from the standard rather than from
269
- defects:
270
-
271
- - **+1** `Consulta Apontamento Diário` is an EQ in the reference; AFP §6.5.3
272
- requires collapsing EQ into EO, and an EO weighs more in the same band.
273
- - **−1** `Apontamento c/ Justificativa`: the IFPUG manual counts 1 DET for the
274
- user message, AFP does not.
275
-
276
- They cancel out, which is exactly why the total is reported alongside the
277
- function-by-function agreement rather than on its own.
278
-
279
- Reproduce it with `npm test` — the benchmark is
280
- `tests/acceptance/vazquez.spec.ts`, and the reference is
281
- [`tests/fixtures/apps/vazquez/REFERENCE.md`](tests/fixtures/apps/vazquez/REFERENCE.md).
282
-
283
- ## Principles
284
-
285
- **Traceability.** Every counted function says where it came from: file, line,
286
- rule applied, origin of each DET and each FTR, and the path walked through the
287
- call graph. The ruleset is versioned and printed in every report — two counts
288
- are only comparable if the rules did not change in between.
289
-
290
- **Say "I don't know" rather than be wrong in silence.** A call the tracer cannot
291
- follow enters the coverage metric. If coverage falls below the configured
292
- threshold, the analysis **fails** instead of emitting a number that looks right.
293
- This is not a preference; AFP §6.5.3 requires it:
294
-
295
- > "If the transaction execution depends on code that is unknown or unavailable
296
- > to the automated tool, the code end point shall be cataloged and listed in the
297
- > generated report in order to detect and quantify the missing patterns and
298
- > libraries."
299
-
300
- **Shape must not change the count.** The same logical application written in
301
- different ways — flat MVC or module-per-domain, fat controller or action object,
302
- generated artefacts or none — must produce an identical number. That is the
303
- project's golden invariant, and it is a test
304
- (`tests/acceptance/golden_invariant.spec.ts`) that was written before the first
305
- collector.
306
-
307
- **Extensibility as a requirement.** AdonisJS imposes no code organisation — fat
308
- controller, action object, static service, injected service, module function,
309
- job. Tracing strategies are registrable, so a project with its own convention
310
- registers it (see [Custom code pattern](#custom-code-pattern)).
311
-
312
- **Function points are not the only number on the dashboard.** If function points
313
- pay, the team optimises function points: more models, more endpoints, less
314
- reuse. Coupling, instability and density come free from the same inventory, and
315
- 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).
316
132
 
317
133
  ## Configuration
318
134
 
@@ -332,10 +148,11 @@ export default defineConfig({
332
148
  infrastructure: ['access_tokens', 'audits'], // excluded, with the reason in the report
333
149
  externallyMaintained: ['erp_customers'], // counted as EIF instead of ILF
334
150
  business: ['chat_sessions'], // the AFP naming filter caught it by accident
151
+ // technicalPatterns: [...DEFAULT_TECHNICAL_PATTERNS], // replaces the filter's naming list
335
152
  ignoreEntryPoints: ['prometheus.metrics'],
336
153
  },
337
154
 
338
- retStrategy: 'constant', // or 'composition'
155
+ dataFunctions: { grouping: 'usage' }, // 'none' keeps every table its own data function
339
156
  maxDepth: 3, // how far to follow the call graph
340
157
  messageDet: 0, // 1 restores the IFPUG confirmation-message DET
341
158
  minCoverage: 0.85, // below this, the analysis fails
@@ -391,22 +208,25 @@ over a rate limiter or an attachment variant is a real example. Without a way
391
208
  to say so, such a call stays unresolved and drags the coverage gate down.
392
209
 
393
210
  ```ts
394
- const limiterIsDataFree: CallResolver = {
395
- name: 'login-limiter',
396
- order: 1,
397
- resolve: () => [],
398
- ignores(call) {
399
- // true means: this is mine, and it touches no data store
400
- 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
+ ],
401
219
  },
402
- }
220
+ })
403
221
  ```
404
222
 
405
- `ignores` is asked before `resolve`, in the same order, so a later and more
406
- generic strategy cannot follow the call into a body it has no business reading.
407
- It is deliberately more expensive than a list of method names to silence: the
408
- volume still appears in the confidence block of `fp:count`, because a silent
409
- 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.
410
230
 
411
231
  ## Support
412
232
 
@@ -426,9 +246,12 @@ Inherited from the AFP standard itself, not from this implementation:
426
246
  - **EQ is collapsed into EO.** Telling an inquiry from an output requires
427
247
  knowing whether there is derived data or calculation, which static analysis
428
248
  cannot see. AFP mandates the collapse.
429
- - **RET is approximated.** What a user recognises as a logical subgroup is not
430
- derivable from code. The default pins it at 1; `composition` derives it from
431
- 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.
432
255
  - **Confirmation and error messages** count 1 DET in a manual count and are
433
256
  invisible here — a known systematic divergence of −1 DET per transaction.
434
257
  `messageDet: 1` restores it.
@@ -436,44 +259,42 @@ Inherited from the AFP standard itself, not from this implementation:
436
259
  judgement. AFP fixes VAF = 1, and the unadjusted count is what public
437
260
  contracts in Brazil use anyway.
438
261
  - **The modification factor in `fp:diff` is 1.** AEP grades it from 0.25 to
439
- 1.75 using Effort Complexity, which requires cyclomatic complexity. A flat 1
440
- does not discriminate — it prices a one-line fix and a rewrite the same — and
441
- the report says so.
442
-
443
- What it does discriminate is **why** a function changed, which is usually the
444
- larger question:
445
-
446
- ```
447
- changed 74 functions 318 FP × 1
448
- type 1 functions 3 FP reclassified, e.g. EO -> EI
449
- size 28 functions 141 FP DET or FTR moved
450
- implementation 45 functions 174 FP same size, different code
451
- ```
452
-
453
- On a real month of work that is 44% of the invoice coming from refactoring.
454
- Whether that should be billed at full value is a contract decision, not a
455
- counting one — but it has to be visible before anyone can make it.
456
-
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.
457
268
  - **Schema-driven applications undercount their input.** When the fields a user
458
269
  fills live in a JSON column whose schema is stored in the database, there is
459
- nothing for static analysis to read: each opaque column counts as 1 DET.
460
- Measured on a production application, the effect is about 2% of the total —
461
- data functions are unaffected, and only the form-submission transaction loses
462
- complexity.
463
-
464
- `fp:count` names every opaque column a transaction reaches, so the limitation
465
- is visible where you read the number rather than only in a design document.
466
- The way out is to declare the number rather than let the tool guess it:
467
- `overrides: { 'POST /petitions': { det: 42, reason: '…' } }`. The reason is
468
- required by the type, `fp:explain` prints it beside the number, and `fp:count`
469
- reports what share of the total was declared — because an override is right
470
- where static analysis is blind and poison as a habit. See
471
- 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.
472
285
 
473
286
  - **Only HTTP routes are collected as entry points.** An ace command that
474
287
  imports a spreadsheet and a scheduled job are transactional functions under
475
288
  IFPUG; they are out of v1.
476
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
+
477
298
  ## References
478
299
 
479
300
  - **OMG Automated Function Points (AFP) 1.0** — ISO/IEC 19515:2019. The
@@ -495,44 +316,32 @@ Inherited from the AFP standard itself, not from this implementation:
495
316
 
496
317
  The reasoning behind the count lives with the code:
497
318
 
498
- - [`docs/design/architecture.md`](docs/design/architecture.md) — the thesis, the
499
- 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
500
321
  - [`docs/design/counting-decisions.md`](docs/design/counting-decisions.md) —
501
322
  each edge case, with the AFP rule that settles it
502
323
  - [`docs/design/resolvers.md`](docs/design/resolvers.md) — the catalogue of code
503
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)
504
327
 
505
328
  Three further documents are kept as a **dated record** of how the design was
506
329
  arrived at, in Portuguese, and are not a reference for current behaviour:
507
- [`implementation-plan.md`](docs/design/implementation-plan.md) (built phase by
508
- phase, and what each phase found),
509
- [`adonisjs-variation.md`](docs/research/adonisjs-variation.md) (what varies
510
- between real AdonisJS applications) and
511
- [`external-validation.md`](docs/research/external-validation.md) (the thesis
512
- 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).
513
333
 
514
334
  ## Contributing
515
335
 
516
336
  ```bash
517
337
  pnpm install
518
- pnpm test # lint + 242 tests, from source
519
- pnpm run typecheck
520
- 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
521
340
  ```
522
341
 
523
- `test:package` is separate on purpose: the suite runs from source through
524
- ts-exec and never loads `build/`, which is the only thing a user gets. A
525
- release once had every `exports` path pointing at a file the build did not
526
- emit, with the whole suite green.
527
-
528
- Two house rules worth knowing before opening a PR:
529
-
530
- 1. **Example first.** A fixture with a known answer comes before the code. A
531
- fixture written after the code tests what the code does, not what it should
532
- do.
533
- 2. **A silent drop is the worst possible defect.** Anything the tracer cannot
534
- follow must land in `unresolved` with the _right_ reason, never be quietly
535
- 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).
536
345
 
537
346
  ## License
538
347