@filipebraida/adonis-function-points 0.5.0 → 0.7.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 +170 -0
- package/README.md +101 -292
- package/build/{calibration-8eV8CEix.js → calibration-DVIf8hcE.js} +42 -3
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-DUbHiifm.js → fp_calibrate-3TxGdS1b.js} +1 -1
- package/build/{fp_count-ChtblhZV.js → fp_count-arGLnVlY.js} +1 -1
- package/build/{fp_diff-Dt7J4IWu.js → fp_diff-DBBvzq5x.js} +1 -1
- package/build/{fp_explain-DZJ--0-S.js → fp_explain-aNFApwiT.js} +1 -1
- package/build/{fp_inventory-CPtmuuke.js → fp_inventory-DIjKIC9t.js} +1 -1
- package/build/{fp_metrics-et8F1Wvt.js → fp_metrics-BpU61waG.js} +1 -1
- package/build/index.d.ts +8 -4
- package/build/index.js +4 -4
- package/build/{pipeline-CNTBhs6o.js → pipeline-DO2301fV.js} +2131 -389
- package/build/{resolvers-PJwo2Z8R.js → resolvers-DaU4uAqT.js} +603 -165
- package/build/{runners-DIt1G85i.js → runners-Dm7cWGa-.js} +6 -3
- package/build/src/albrecht/counter.d.ts +38 -5
- package/build/src/albrecht/data_functions.d.ts +49 -3
- package/build/src/albrecht/diff.d.ts +27 -0
- package/build/src/albrecht/index.d.ts +1 -0
- package/build/src/albrecht/opaque.d.ts +90 -0
- package/build/src/albrecht/technical_filter.d.ts +18 -11
- package/build/src/albrecht/transactional_functions.d.ts +7 -0
- package/build/src/cli.js +2 -2
- package/build/src/define_config.d.ts +55 -57
- package/build/src/inventory/graph/call_graph.d.ts +44 -0
- package/build/src/inventory/graph/deliveries.d.ts +88 -0
- package/build/src/inventory/graph/output_fields.d.ts +143 -0
- package/build/src/inventory/paths.d.ts +2 -0
- package/build/src/inventory/resolvers/index.d.ts +21 -0
- package/build/src/inventory/resolvers/index.js +2 -2
- package/build/src/inventory/resolvers/job_dispatch.d.ts +20 -0
- package/build/src/inventory/resolvers/local_function.d.ts +24 -0
- package/build/src/inventory/resolvers/transformer.d.ts +0 -23
- package/build/src/inventory/sources/commands.d.ts +14 -0
- package/build/src/inventory/sources/jobs.d.ts +27 -0
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +49 -1
- package/build/stubs/config.stub +29 -16
- package/package.json +1 -1
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.
|
|
16
|
+
Ruleset: afp@1.6.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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
116
|
+
### `fp:metrics` — the counterweight
|
|
251
117
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
440
|
-
does not
|
|
441
|
-
the report says so.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
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)
|
|
508
|
-
|
|
509
|
-
[`
|
|
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
|
|
519
|
-
pnpm run
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//#region src/define_config.ts
|
|
2
2
|
const DEFAULTS = {
|
|
3
3
|
boundary: {},
|
|
4
|
-
|
|
4
|
+
dataFunctions: { grouping: "usage" },
|
|
5
5
|
maxDepth: 3,
|
|
6
6
|
messageDet: 0
|
|
7
7
|
};
|
|
@@ -12,6 +12,10 @@ function defineConfig(config) {
|
|
|
12
12
|
boundary: {
|
|
13
13
|
...DEFAULTS.boundary,
|
|
14
14
|
...config.boundary
|
|
15
|
+
},
|
|
16
|
+
dataFunctions: {
|
|
17
|
+
...DEFAULTS.dataFunctions,
|
|
18
|
+
...config.dataFunctions
|
|
15
19
|
}
|
|
16
20
|
};
|
|
17
21
|
}
|
|
@@ -51,6 +55,39 @@ const AEP_FACTORS = {
|
|
|
51
55
|
unchanged: 0
|
|
52
56
|
};
|
|
53
57
|
/**
|
|
58
|
+
* The Roteiro de Métricas de Software do SISP, v3.0 (Portaria SGD/MGI nº 3656,
|
|
59
|
+
* de 2026), §7.3 "Projeto de Melhoria" — what a Brazilian public contract names
|
|
60
|
+
* instead of AEP:
|
|
61
|
+
*
|
|
62
|
+
* PF_MELHORIA = PF_INCLUÍDO + FI × PF_ALTERADO + 0,50 × PF_EXCLUÍDO + PF_CONVERSÃO
|
|
63
|
+
*
|
|
64
|
+
* where FI, the impact factor on an altered function, is 63% when the contractor
|
|
65
|
+
* developed or already maintains the function, and 84% when it did not (and must
|
|
66
|
+
* document it). This preset carries the 63% — a factory billing maintenance of
|
|
67
|
+
* its own work — and `diff.factors: { changed: 0.84 }` is the other case.
|
|
68
|
+
* PF_CONVERSÃO is data conversion, which this package does not count.
|
|
69
|
+
*
|
|
70
|
+
* Read from the guide's own PDF, not from memory: an earlier draft of this
|
|
71
|
+
* preset said 0,50 / 0,30, and v2.0 (2012) priced exclusion at 0,40. A contract
|
|
72
|
+
* binds to a revision, so the report prints which preset priced the total.
|
|
73
|
+
*/
|
|
74
|
+
const SISP_FACTORS = {
|
|
75
|
+
added: 1,
|
|
76
|
+
changed: .63,
|
|
77
|
+
removed: .5,
|
|
78
|
+
unchanged: 0
|
|
79
|
+
};
|
|
80
|
+
const FACTOR_PRESETS = {
|
|
81
|
+
aep: {
|
|
82
|
+
label: "OMG Automated Enhancement Points 1.0, §6.5",
|
|
83
|
+
factors: AEP_FACTORS
|
|
84
|
+
},
|
|
85
|
+
sisp: {
|
|
86
|
+
label: "Roteiro de Métricas de Software do SISP v3.0 §7.3, FI 63% (own maintenance); 84% otherwise",
|
|
87
|
+
factors: SISP_FACTORS
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
54
91
|
* Two counts of DIFFERENT applications compare cleanly and mean nothing.
|
|
55
92
|
*
|
|
56
93
|
* The ruleset guard already refuses counts produced by different rules. This
|
|
@@ -68,8 +105,9 @@ var IncomparableSourcesError = class extends Error {
|
|
|
68
105
|
function diffCounts(from, to, options = {}) {
|
|
69
106
|
if (from.rulesetVersion !== to.rulesetVersion || from.ruleset !== to.ruleset) throw new IncomparableRulesetsError(`${from.ruleset}@${from.rulesetVersion}`, `${to.ruleset}@${to.rulesetVersion}`);
|
|
70
107
|
if (from.source && to.source && from.source.app !== to.source.app) throw new IncomparableSourcesError(from.source.app, to.source.app);
|
|
108
|
+
const preset = options.preset ?? "aep";
|
|
71
109
|
const factors = {
|
|
72
|
-
...
|
|
110
|
+
...FACTOR_PRESETS[preset].factors,
|
|
73
111
|
...options.factors
|
|
74
112
|
};
|
|
75
113
|
const reasonFactors = options.reasonFactors ?? {};
|
|
@@ -140,6 +178,7 @@ function diffCounts(from, to, options = {}) {
|
|
|
140
178
|
* an invoice, and a reader who sees that tail stops trusting the rest.
|
|
141
179
|
*/
|
|
142
180
|
billable: round2(entries.reduce((total, entry) => total + entry.function.points * factorFor(entry), 0)),
|
|
181
|
+
preset,
|
|
143
182
|
factors,
|
|
144
183
|
reasonFactors,
|
|
145
184
|
warnings
|
|
@@ -400,4 +439,4 @@ function parseSamples(csv) {
|
|
|
400
439
|
return samples;
|
|
401
440
|
}
|
|
402
441
|
//#endregion
|
|
403
|
-
export { AEP_FACTORS as a,
|
|
442
|
+
export { AEP_FACTORS as a, IncomparableSourcesError as c, DEFAULTS as d, defineConfig as f, measureStructure as i, SISP_FACTORS as l, parseSamples as n, FACTOR_PRESETS as o, measureConformance as r, IncomparableRulesetsError as s, calibrate as t, diffCounts as u };
|
package/build/commands/main.js
CHANGED
|
@@ -14,27 +14,27 @@
|
|
|
14
14
|
const commands = [
|
|
15
15
|
{
|
|
16
16
|
commandName: "fp:inventory",
|
|
17
|
-
importer: () => import("../fp_inventory-
|
|
17
|
+
importer: () => import("../fp_inventory-DIjKIC9t.js")
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
commandName: "fp:metrics",
|
|
21
|
-
importer: () => import("../fp_metrics-
|
|
21
|
+
importer: () => import("../fp_metrics-BpU61waG.js")
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
commandName: "fp:count",
|
|
25
|
-
importer: () => import("../fp_count-
|
|
25
|
+
importer: () => import("../fp_count-arGLnVlY.js")
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
commandName: "fp:explain",
|
|
29
|
-
importer: () => import("../fp_explain-
|
|
29
|
+
importer: () => import("../fp_explain-aNFApwiT.js")
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
commandName: "fp:diff",
|
|
33
|
-
importer: () => import("../fp_diff-
|
|
33
|
+
importer: () => import("../fp_diff-DBBvzq5x.js")
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
commandName: "fp:calibrate",
|
|
37
|
-
importer: () => import("../fp_calibrate-
|
|
37
|
+
importer: () => import("../fp_calibrate-3TxGdS1b.js")
|
|
38
38
|
}
|
|
39
39
|
];
|
|
40
40
|
let cache = null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as printResult, t as runCalibrate } from "./runners-
|
|
1
|
+
import { s as printResult, t as runCalibrate } from "./runners-Dm7cWGa-.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-
|
|
1
|
+
import { n as runCount, s as printResult } from "./runners-Dm7cWGa-.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-
|
|
1
|
+
import { r as runDiff, s as printResult } from "./runners-Dm7cWGa-.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-
|
|
1
|
+
import { i as runExplain, s as printResult } from "./runners-Dm7cWGa-.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
|