@filipebraida/adonis-function-points 0.1.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/LICENSE.md +21 -0
- package/README.md +427 -0
- package/bin/cli.js +4 -0
- package/build/commands/fp_calibrate.d.ts +16 -0
- package/build/commands/fp_count.d.ts +11 -0
- package/build/commands/fp_diff.d.ts +16 -0
- package/build/commands/fp_explain.d.ts +15 -0
- package/build/commands/fp_inventory.d.ts +9 -0
- package/build/commands/main.d.ts +5 -0
- package/build/commands/main.js +120 -0
- package/build/commands/printer.d.ts +9 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +10 -0
- package/build/define_config-DOqWyPwV.js +19 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +4 -0
- package/build/pipeline-BzP-ITGN.js +2306 -0
- package/build/resolvers-CU9HKYpn.js +555 -0
- package/build/runners-Bt8tbISi.js +630 -0
- package/build/scripts/smoke_package.d.ts +1 -0
- package/build/src/albrecht/calibration.d.ts +62 -0
- package/build/src/albrecht/counter.d.ts +48 -0
- package/build/src/albrecht/data_functions.d.ts +32 -0
- package/build/src/albrecht/diff.d.ts +66 -0
- package/build/src/albrecht/index.d.ts +13 -0
- package/build/src/albrecht/tables.d.ts +19 -0
- package/build/src/albrecht/technical_filter.d.ts +25 -0
- package/build/src/albrecht/transactional_functions.d.ts +35 -0
- package/build/src/cli/load_config.d.ts +28 -0
- package/build/src/cli/print.d.ts +19 -0
- package/build/src/cli/runners.d.ts +52 -0
- package/build/src/cli.d.ts +19 -0
- package/build/src/cli.js +198 -0
- package/build/src/define_config.d.ts +137 -0
- package/build/src/inventory/app_context.d.ts +73 -0
- package/build/src/inventory/detectors/lucid.d.ts +77 -0
- package/build/src/inventory/graph/call_graph.d.ts +80 -0
- package/build/src/inventory/graph/noise.d.ts +9 -0
- package/build/src/inventory/index.d.ts +15 -0
- package/build/src/inventory/paths.d.ts +22 -0
- package/build/src/inventory/resolvers/action_object.d.ts +14 -0
- package/build/src/inventory/resolvers/index.d.ts +23 -0
- package/build/src/inventory/resolvers/index.js +2 -0
- package/build/src/inventory/resolvers/job_dispatch.d.ts +18 -0
- package/build/src/inventory/resolvers/module_function.d.ts +11 -0
- package/build/src/inventory/resolvers/property_service.d.ts +18 -0
- package/build/src/inventory/resolvers/same_class_method.d.ts +17 -0
- package/build/src/inventory/resolvers/static_service.d.ts +13 -0
- package/build/src/inventory/resolvers/transformer.d.ts +25 -0
- package/build/src/inventory/resolvers/types.d.ts +65 -0
- package/build/src/inventory/source.d.ts +39 -0
- package/build/src/inventory/sources/data_stores.d.ts +31 -0
- package/build/src/inventory/sources/json_schemas.d.ts +32 -0
- package/build/src/inventory/sources/routes_ast.d.ts +28 -0
- package/build/src/metrics/structure.d.ts +72 -0
- package/build/src/pipeline.d.ts +44 -0
- package/build/src/pipeline.js +2 -0
- package/build/src/reporters/table.d.ts +6 -0
- package/build/src/types.d.ts +256 -0
- package/build/src/types.js +1 -0
- package/build/stubs/config.stub +37 -0
- package/build/tmp/probe.d.ts +1 -0
- package/build/tmp/probe_cli.d.ts +1 -0
- package/build/tmp/probe_cmp.d.ts +1 -0
- package/build/tmp/probe_count.d.ts +1 -0
- package/build/tmp/probe_data.d.ts +1 -0
- package/build/tmp/probe_diff.d.ts +1 -0
- package/build/tmp/probe_gap.d.ts +1 -0
- package/build/tmp/probe_graph.d.ts +1 -0
- package/build/tmp/probe_metrics.d.ts +1 -0
- package/build/tmp/probe_miss.d.ts +1 -0
- package/build/tmp/probe_names.d.ts +1 -0
- package/build/tmp/probe_nodata.d.ts +1 -0
- package/build/tmp/probe_one.d.ts +1 -0
- package/build/tmp/probe_perf.d.ts +1 -0
- package/build/tmp/probe_routes.d.ts +1 -0
- package/build/tmp/probe_unres.d.ts +1 -0
- package/build/tmp/probe_vazquez.d.ts +1 -0
- package/build/tsdown.config.d.ts +2 -0
- package/package.json +133 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Filipe Braida
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# @filipebraida/adonis-function-points
|
|
2
|
+
|
|
3
|
+
Automated function point counting and code metrics for AdonisJS applications.
|
|
4
|
+
|
|
5
|
+
Counts IFPUG function points straight from the source, following the OMG
|
|
6
|
+
**Automated Function Points** standard (ISO/IEC 19515). Every number it prints
|
|
7
|
+
says where it came from: the file, the line, the rule from the standard, and the
|
|
8
|
+
origin of each DET and each FTR.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node ace fp:count
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Unadjusted count: 46 FP
|
|
16
|
+
Ruleset: afp@1.0.0
|
|
17
|
+
|
|
18
|
+
type n FP
|
|
19
|
+
ILF 2 14
|
|
20
|
+
EIF 1 5
|
|
21
|
+
EI 4 13
|
|
22
|
+
EO 3 14
|
|
23
|
+
|
|
24
|
+
function type DET FTR FP
|
|
25
|
+
Apontamento ILF 4 1 7
|
|
26
|
+
Justificativa ILF 3 1 7
|
|
27
|
+
Pessoa EIF 4 1 5
|
|
28
|
+
GET /apontamentos EO 4 1 4
|
|
29
|
+
POST /apontamentos EI 3 1 3
|
|
30
|
+
PUT /apontamentos/:param EI 5 2 4
|
|
31
|
+
DELETE /apontamentos/:param EI 1 2 3
|
|
32
|
+
POST /apontamentos/justificar EI 4 2 3
|
|
33
|
+
GET /presenca EO 13 3 5
|
|
34
|
+
GET /presenca/relatorio EO 13 3 5
|
|
35
|
+
```
|
|
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
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm i @filipebraida/adonis-function-points
|
|
53
|
+
node ace configure @filipebraida/adonis-function-points
|
|
54
|
+
```
|
|
55
|
+
|
|
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:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx @filipebraida/adonis-function-points count --root ./my-app
|
|
64
|
+
```
|
|
65
|
+
|
|
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.
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
| command | what it does |
|
|
134
|
+
| ------------------------------------- | ---------------------------------------------------------- |
|
|
135
|
+
| `node ace fp:count` | counts unadjusted function points |
|
|
136
|
+
| `node ace fp:inventory` | the raw facts: stores, routes, tracing coverage |
|
|
137
|
+
| `node ace fp:explain <name>` | why one function was counted that way |
|
|
138
|
+
| `node ace fp:diff <previous.json>` | additions / modifications / deletions, and the billable FP |
|
|
139
|
+
| `node ace fp:calibrate <samples.csv>` | correction factors against a manual count |
|
|
140
|
+
|
|
141
|
+
`fp:count --out count.json` saves a count; `fp:diff count.json` compares that
|
|
142
|
+
saved count against the current state of the application. It deliberately does
|
|
143
|
+
**not** take a git ref: booting an older checkout, with possibly different
|
|
144
|
+
dependencies, is a problem not worth solving.
|
|
145
|
+
|
|
146
|
+
### `fp:explain` — the number has to be defensible
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
POST /apontamentos — EI, low complexity, 3 FP
|
|
150
|
+
module: ponto
|
|
151
|
+
|
|
152
|
+
Rule applied: afp:6.5.3 modifies a data store -> EI
|
|
153
|
+
|
|
154
|
+
DET = 3
|
|
155
|
+
validator:registrarPontoValidator.marcadoEm
|
|
156
|
+
validator:registrarPontoValidator.pessoaId
|
|
157
|
+
validator:registrarPontoValidator.tipo
|
|
158
|
+
|
|
159
|
+
FTR = 1
|
|
160
|
+
reaches:Apontamento
|
|
161
|
+
|
|
162
|
+
Path walked:
|
|
163
|
+
controllers/apontamentos_controller.ts#store (store)
|
|
164
|
+
actions/registrar_ponto.ts#handle (action-object) [writes]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If function points get invoiced, someone will dispute a number — and a number
|
|
168
|
+
without provenance is indefensible.
|
|
169
|
+
|
|
170
|
+
## Benchmark
|
|
171
|
+
|
|
172
|
+
The only reference in this project not produced by its own authors is the case
|
|
173
|
+
study published in **Vazquez, Simões & Albert (2011)**, the same one used by the
|
|
174
|
+
COPPE/UFRJ dissertation on the _Ligeiro_ tool (Pinel, 2012).
|
|
175
|
+
|
|
176
|
+
The fixture and the reference count were frozen in their own commit **before**
|
|
177
|
+
the counter was ever run against them, with the transcription choices written
|
|
178
|
+
down first. Without that the independence would be illusory.
|
|
179
|
+
|
|
180
|
+
| | total | vs reference |
|
|
181
|
+
| ----------------------------------- | --------- | ------------ |
|
|
182
|
+
| **Vazquez et al. (2011), manual** | **46 FP** | — |
|
|
183
|
+
| **this package** | **46 FP** | **0%** |
|
|
184
|
+
| Ligeiro, automated (Pinel 2012) | 52 FP | +13% |
|
|
185
|
+
| Ligeiro, manual under its own rules | 43 FP | −6.5% |
|
|
186
|
+
|
|
187
|
+
Eight of the ten functions match exactly. The two that do not were **predicted
|
|
188
|
+
in writing before the run**, and come from the standard rather than from
|
|
189
|
+
defects:
|
|
190
|
+
|
|
191
|
+
- **+1** `Consulta Apontamento Diário` is an EQ in the reference; AFP §6.5.3
|
|
192
|
+
requires collapsing EQ into EO, and an EO weighs more in the same band.
|
|
193
|
+
- **−1** `Apontamento c/ Justificativa`: the IFPUG manual counts 1 DET for the
|
|
194
|
+
user message, AFP does not.
|
|
195
|
+
|
|
196
|
+
They cancel out, which is exactly why the total is reported alongside the
|
|
197
|
+
function-by-function agreement rather than on its own.
|
|
198
|
+
|
|
199
|
+
Reproduce it with `npm test` — the benchmark is
|
|
200
|
+
`tests/acceptance/vazquez.spec.ts`, and the reference is
|
|
201
|
+
[`tests/fixtures/apps/vazquez/REFERENCE.md`](tests/fixtures/apps/vazquez/REFERENCE.md).
|
|
202
|
+
|
|
203
|
+
## Principles
|
|
204
|
+
|
|
205
|
+
**Traceability.** Every counted function says where it came from: file, line,
|
|
206
|
+
rule applied, origin of each DET and each FTR, and the path walked through the
|
|
207
|
+
call graph. The ruleset is versioned and printed in every report — two counts
|
|
208
|
+
are only comparable if the rules did not change in between.
|
|
209
|
+
|
|
210
|
+
**Say "I don't know" rather than be wrong in silence.** A call the tracer cannot
|
|
211
|
+
follow enters the coverage metric. If coverage falls below the configured
|
|
212
|
+
threshold, the analysis **fails** instead of emitting a number that looks right.
|
|
213
|
+
This is not a preference; AFP §6.5.3 requires it:
|
|
214
|
+
|
|
215
|
+
> "If the transaction execution depends on code that is unknown or unavailable
|
|
216
|
+
> to the automated tool, the code end point shall be cataloged and listed in the
|
|
217
|
+
> generated report in order to detect and quantify the missing patterns and
|
|
218
|
+
> libraries."
|
|
219
|
+
|
|
220
|
+
**Shape must not change the count.** The same logical application written in
|
|
221
|
+
different ways — flat MVC or module-per-domain, fat controller or action object,
|
|
222
|
+
generated artefacts or none — must produce an identical number. That is the
|
|
223
|
+
project's golden invariant, and it is a test
|
|
224
|
+
(`tests/acceptance/golden_invariant.spec.ts`) that was written before the first
|
|
225
|
+
collector.
|
|
226
|
+
|
|
227
|
+
**Extensibility as a requirement.** AdonisJS imposes no code organisation — fat
|
|
228
|
+
controller, action object, static service, injected service, module function,
|
|
229
|
+
job. Tracing strategies are registrable, so a project with its own convention
|
|
230
|
+
registers it (see [Custom code pattern](#custom-code-pattern)).
|
|
231
|
+
|
|
232
|
+
**Function points are not the only number on the dashboard.** If function points
|
|
233
|
+
pay, the team optimises function points: more models, more endpoints, less
|
|
234
|
+
reuse. Coupling, instability and density come free from the same inventory, and
|
|
235
|
+
are the counterweight.
|
|
236
|
+
|
|
237
|
+
## Configuration
|
|
238
|
+
|
|
239
|
+
Discovery does the technical work — subpath aliases, generated artefacts,
|
|
240
|
+
layout, scan roots are all read from the application, never assumed. What stays
|
|
241
|
+
configurable is what is a **business decision** that no heuristic should make.
|
|
242
|
+
|
|
243
|
+
**Every option here has an effect, and a test proving it.** Configuration the
|
|
244
|
+
code does not honour is worse than none at all.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// config/function_points.ts
|
|
248
|
+
import { defineConfig } from '@filipebraida/adonis-function-points'
|
|
249
|
+
|
|
250
|
+
export default defineConfig({
|
|
251
|
+
boundary: {
|
|
252
|
+
infrastructure: ['access_tokens', 'audits'], // excluded, with the reason in the report
|
|
253
|
+
externallyMaintained: ['erp_customers'], // counted as EIF instead of ILF
|
|
254
|
+
ignoreEntryPoints: ['prometheus.metrics'],
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
retStrategy: 'constant', // or 'composition'
|
|
258
|
+
maxDepth: 3, // how far to follow the call graph
|
|
259
|
+
messageDet: 0, // 1 restores the IFPUG confirmation-message DET
|
|
260
|
+
minCoverage: 0.85, // below this, the analysis fails
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`complexityTables` and `weights` are also accepted, for calibrating the bands
|
|
265
|
+
against a manual count.
|
|
266
|
+
|
|
267
|
+
Both front-ends load this file from the application root, and every run prints
|
|
268
|
+
which configuration produced it — the file path, or `defaults` when there is
|
|
269
|
+
none. A configuration file that exists and fails to load is an **error**: the
|
|
270
|
+
count is not produced. Falling back to the defaults with a warning would change
|
|
271
|
+
the number without telling anyone, and the number becomes an invoice.
|
|
272
|
+
|
|
273
|
+
### Custom code pattern
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import type { CallResolver } from '@filipebraida/adonis-function-points'
|
|
277
|
+
|
|
278
|
+
const repositoryResolver: CallResolver = {
|
|
279
|
+
name: 'my-repository',
|
|
280
|
+
order: 5, // lower runs first; custom strategies run before the built-ins
|
|
281
|
+
resolve(call, ctx) {
|
|
282
|
+
// return the bodies to follow, or [] if this is not your pattern
|
|
283
|
+
return []
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default defineConfig({
|
|
288
|
+
resolvers: { call: [repositoryResolver] },
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The **first** strategy that claims a call wins. That is not an implementation
|
|
293
|
+
detail: `CreateUserJob.dispatch(p)`, `UserService.create(p)` and `User.find(p)`
|
|
294
|
+
are all `Identifier.method(args)`, and only ordering tells them apart.
|
|
295
|
+
|
|
296
|
+
Built-in strategies, most specific first: `same-class-method`, `action-object`,
|
|
297
|
+
`job-dispatch`, `static-service`, `property-service`, `module-function`.
|
|
298
|
+
|
|
299
|
+
## Support
|
|
300
|
+
|
|
301
|
+
| | v1 |
|
|
302
|
+
| --------------------- | ------------------------------------------------------------ |
|
|
303
|
+
| AdonisJS 7 + Lucid 22 | **yes** — generated schema, `codegen`, with or without Tuyau |
|
|
304
|
+
| AdonisJS 6 / Lucid 21 | no — detected and reported |
|
|
305
|
+
| Kysely and other ORMs | no — detected and reported |
|
|
306
|
+
|
|
307
|
+
Out of scope, the package says it does not support the application. It never
|
|
308
|
+
counts zero in silence.
|
|
309
|
+
|
|
310
|
+
## Known limitations
|
|
311
|
+
|
|
312
|
+
Inherited from the AFP standard itself, not from this implementation:
|
|
313
|
+
|
|
314
|
+
- **EQ is collapsed into EO.** Telling an inquiry from an output requires
|
|
315
|
+
knowing whether there is derived data or calculation, which static analysis
|
|
316
|
+
cannot see. AFP mandates the collapse.
|
|
317
|
+
- **RET is approximated.** What a user recognises as a logical subgroup is not
|
|
318
|
+
derivable from code. The default pins it at 1; `composition` derives it from
|
|
319
|
+
composition relations.
|
|
320
|
+
- **Confirmation and error messages** count 1 DET in a manual count and are
|
|
321
|
+
invisible here — a known systematic divergence of −1 DET per transaction.
|
|
322
|
+
`messageDet: 1` restores it.
|
|
323
|
+
- **VAF is not calculated.** The 14 general system characteristics require human
|
|
324
|
+
judgement. AFP fixes VAF = 1, and the unadjusted count is what public
|
|
325
|
+
contracts in Brazil use anyway.
|
|
326
|
+
- **The modification factor in `fp:diff` is 1.** AEP grades it from 0.25 to
|
|
327
|
+
1.75 using Effort Complexity, which requires cyclomatic complexity. A flat 1
|
|
328
|
+
does not discriminate — it prices a one-line fix and a rewrite the same — and
|
|
329
|
+
the report says so.
|
|
330
|
+
|
|
331
|
+
What it does discriminate is **why** a function changed, which is usually the
|
|
332
|
+
larger question:
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
changed 74 functions 318 FP × 1
|
|
336
|
+
type 1 functions 3 FP reclassified, e.g. EO -> EI
|
|
337
|
+
size 28 functions 141 FP DET or FTR moved
|
|
338
|
+
implementation 45 functions 174 FP same size, different code
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
On a real month of work that is 44% of the invoice coming from refactoring.
|
|
342
|
+
Whether that should be billed at full value is a contract decision, not a
|
|
343
|
+
counting one — but it has to be visible before anyone can make it.
|
|
344
|
+
|
|
345
|
+
- **Schema-driven applications undercount their input.** When the fields a user
|
|
346
|
+
fills live in a JSON column whose schema is stored in the database, there is
|
|
347
|
+
nothing for static analysis to read: each opaque column counts as 1 DET.
|
|
348
|
+
Measured on a production application, the effect is about 2% of the total —
|
|
349
|
+
data functions are unaffected, and only the form-submission transaction loses
|
|
350
|
+
complexity.
|
|
351
|
+
|
|
352
|
+
`fp:count` names every opaque column a transaction reaches, so the limitation
|
|
353
|
+
is visible where you read the number rather than only in a design document.
|
|
354
|
+
The way out is to declare the number rather than let the tool guess it:
|
|
355
|
+
`overrides: { 'POST /petitions': { det: 42, reason: '…' } }`. The reason is
|
|
356
|
+
required by the type, `fp:explain` prints it beside the number, and `fp:count`
|
|
357
|
+
reports what share of the total was declared — because an override is right
|
|
358
|
+
where static analysis is blind and poison as a habit. See
|
|
359
|
+
counting-decisions §8.
|
|
360
|
+
|
|
361
|
+
- **Only HTTP routes are collected as entry points.** An ace command that
|
|
362
|
+
imports a spreadsheet and a scheduled job are transactional functions under
|
|
363
|
+
IFPUG; they are out of v1.
|
|
364
|
+
|
|
365
|
+
## References
|
|
366
|
+
|
|
367
|
+
- **OMG Automated Function Points (AFP) 1.0** — ISO/IEC 19515:2019. The
|
|
368
|
+
normative basis for the count: technical data filter (§6.5.2.1.1), transaction
|
|
369
|
+
detection (§6.5.3), ILF vs EIF by maintenance (§6.5.4), DET/RET/FTR (§7.2,
|
|
370
|
+
§7.3).
|
|
371
|
+
- **OMG Automated Enhancement Points (AEP) 1.0** — the basis for `fp:diff`:
|
|
372
|
+
added / modified / deleted (§6.3) and the complexity factors (§6.5).
|
|
373
|
+
- **IFPUG Counting Practices Manual (CPM) 4.3** — the underlying method AFP
|
|
374
|
+
automates.
|
|
375
|
+
- **Vazquez, C. E., Simões, G. S., Albert, R. M. (2011).** _Análise de Pontos de
|
|
376
|
+
Função: Medição, Estimativas e Gerenciamento de Projetos de Software._ Érica.
|
|
377
|
+
The benchmark case study.
|
|
378
|
+
- **Pinel, B. (2012).** _Ligeiro: uma ferramenta para contagem automática de
|
|
379
|
+
pontos de função._ COPPE/UFRJ.
|
|
380
|
+
[pesc.coppe.ufrj.br](https://pesc.coppe.ufrj.br/uploadfile/1343153707.pdf)
|
|
381
|
+
|
|
382
|
+
## Design documents
|
|
383
|
+
|
|
384
|
+
The reasoning behind the count lives with the code:
|
|
385
|
+
|
|
386
|
+
- [`docs/design/architecture.md`](docs/design/architecture.md) — the thesis, the
|
|
387
|
+
layers, and what is discovered instead of configured
|
|
388
|
+
- [`docs/design/counting-decisions.md`](docs/design/counting-decisions.md) —
|
|
389
|
+
each edge case, with the AFP rule that settles it
|
|
390
|
+
- [`docs/design/resolvers.md`](docs/design/resolvers.md) — the catalogue of code
|
|
391
|
+
patterns and how each is followed
|
|
392
|
+
|
|
393
|
+
Three further documents are kept as a **dated record** of how the design was
|
|
394
|
+
arrived at, in Portuguese, and are not a reference for current behaviour:
|
|
395
|
+
[`implementation-plan.md`](docs/design/implementation-plan.md) (built phase by
|
|
396
|
+
phase, and what each phase found),
|
|
397
|
+
[`adonisjs-variation.md`](docs/research/adonisjs-variation.md) (what varies
|
|
398
|
+
between real AdonisJS applications) and
|
|
399
|
+
[`external-validation.md`](docs/research/external-validation.md) (the thesis
|
|
400
|
+
tested outside the sample that produced it).
|
|
401
|
+
|
|
402
|
+
## Contributing
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
pnpm install
|
|
406
|
+
pnpm test # lint + 242 tests, from source
|
|
407
|
+
pnpm run typecheck
|
|
408
|
+
pnpm run compile && pnpm run test:package # the packed tarball, installed and used
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`test:package` is separate on purpose: the suite runs from source through
|
|
412
|
+
ts-exec and never loads `build/`, which is the only thing a user gets. A
|
|
413
|
+
release once had every `exports` path pointing at a file the build did not
|
|
414
|
+
emit, with the whole suite green.
|
|
415
|
+
|
|
416
|
+
Two house rules worth knowing before opening a PR:
|
|
417
|
+
|
|
418
|
+
1. **Example first.** A fixture with a known answer comes before the code. A
|
|
419
|
+
fixture written after the code tests what the code does, not what it should
|
|
420
|
+
do.
|
|
421
|
+
2. **A silent drop is the worst possible defect.** Anything the tracer cannot
|
|
422
|
+
follow must land in `unresolved` with the _right_ reason, never be quietly
|
|
423
|
+
treated as a read.
|
|
424
|
+
|
|
425
|
+
## License
|
|
426
|
+
|
|
427
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* Measures the counter's bias against a manual count.
|
|
5
|
+
*
|
|
6
|
+
* It does NOT apply the factor: calibrating is a decision for whoever signs the
|
|
7
|
+
* contract, and a factor applied silently would stop the count from being
|
|
8
|
+
* reproducible from the code.
|
|
9
|
+
*/
|
|
10
|
+
export default class FpCalibrate extends BaseCommand {
|
|
11
|
+
static commandName: string;
|
|
12
|
+
static description: string;
|
|
13
|
+
static options: CommandOptions;
|
|
14
|
+
samples: string;
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
export default class FpCount extends BaseCommand {
|
|
4
|
+
static commandName: string;
|
|
5
|
+
static description: string;
|
|
6
|
+
static options: CommandOptions;
|
|
7
|
+
out?: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
minCoverage?: number;
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* Additions, changes and deletions between two counts — what gets invoiced.
|
|
5
|
+
*
|
|
6
|
+
* It works on a SAVED count (`fp:count --out`) compared against the current
|
|
7
|
+
* state, never on two checkouts: booting the older version, with possibly
|
|
8
|
+
* different dependencies, is a problem not worth solving.
|
|
9
|
+
*/
|
|
10
|
+
export default class FpDiff extends BaseCommand {
|
|
11
|
+
static commandName: string;
|
|
12
|
+
static description: string;
|
|
13
|
+
static options: CommandOptions;
|
|
14
|
+
previous: string;
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* Why a function was counted the way it was.
|
|
5
|
+
*
|
|
6
|
+
* Not a convenience: if function points get invoiced, someone will dispute a
|
|
7
|
+
* number, and a number without provenance is indefensible.
|
|
8
|
+
*/
|
|
9
|
+
export default class FpExplain extends BaseCommand {
|
|
10
|
+
static commandName: string;
|
|
11
|
+
static description: string;
|
|
12
|
+
static options: CommandOptions;
|
|
13
|
+
name: string;
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
export default class FpInventory extends BaseCommand {
|
|
4
|
+
static commandName: string;
|
|
5
|
+
static description: string;
|
|
6
|
+
static options: CommandOptions;
|
|
7
|
+
out?: string;
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as FpInventory } from './fp_inventory.js';
|
|
2
|
+
export { default as FpCount } from './fp_count.js';
|
|
3
|
+
export { default as FpExplain } from './fp_explain.js';
|
|
4
|
+
export { default as FpDiff } from './fp_diff.js';
|
|
5
|
+
export { default as FpCalibrate } from './fp_calibrate.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { a as runInventory, i as runExplain, n as runCount, r as runDiff, s as printResult, t as runCalibrate } from "../runners-Bt8tbISi.js";
|
|
2
|
+
import { BaseCommand, args, flags } from "@adonisjs/core/ace";
|
|
3
|
+
//#region commands/printer.ts
|
|
4
|
+
/**
|
|
5
|
+
* Maps a `RunResult` onto ace's logger.
|
|
6
|
+
*
|
|
7
|
+
* Notes go to `info`, not `log`: they are diagnostics about the run, and under
|
|
8
|
+
* `--json` they must not be mistaken for output.
|
|
9
|
+
*/
|
|
10
|
+
function printerFor(command) {
|
|
11
|
+
return {
|
|
12
|
+
log: (message) => command.logger.log(message),
|
|
13
|
+
error: (message) => command.logger.error(message),
|
|
14
|
+
note: (message) => command.logger.info(message)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
|
|
19
|
+
function __decorate(decorators, target, key, desc) {
|
|
20
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
22
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
23
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region commands/fp_inventory.ts
|
|
27
|
+
var FpInventory = class extends BaseCommand {
|
|
28
|
+
static commandName = "fp:inventory";
|
|
29
|
+
static description = "Extract the raw facts of the application: stores, routes and tracing";
|
|
30
|
+
static options = { startApp: false };
|
|
31
|
+
async run() {
|
|
32
|
+
this.exitCode = printResult(await runInventory({
|
|
33
|
+
root: this.app.makePath(),
|
|
34
|
+
out: this.out
|
|
35
|
+
}), printerFor(this));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
__decorate([flags.string({ description: "Write the inventory as JSON to the given path" })], FpInventory.prototype, "out", void 0);
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region commands/fp_count.ts
|
|
41
|
+
var FpCount = class extends BaseCommand {
|
|
42
|
+
static commandName = "fp:count";
|
|
43
|
+
static description = "Count the unadjusted function points of the application";
|
|
44
|
+
static options = { startApp: false };
|
|
45
|
+
async run() {
|
|
46
|
+
this.exitCode = printResult(await runCount({
|
|
47
|
+
root: this.app.makePath(),
|
|
48
|
+
out: this.out,
|
|
49
|
+
json: this.json,
|
|
50
|
+
minCoverage: this.minCoverage
|
|
51
|
+
}), printerFor(this));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
__decorate([flags.string({ description: "Write the result as JSON to the given path" })], FpCount.prototype, "out", void 0);
|
|
55
|
+
__decorate([flags.boolean({ description: "Print JSON instead of a table" })], FpCount.prototype, "json", void 0);
|
|
56
|
+
__decorate([flags.number({ description: "Minimum tracing coverage (0 to 1)" })], FpCount.prototype, "minCoverage", void 0);
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region commands/fp_explain.ts
|
|
59
|
+
/**
|
|
60
|
+
* Why a function was counted the way it was.
|
|
61
|
+
*
|
|
62
|
+
* Not a convenience: if function points get invoiced, someone will dispute a
|
|
63
|
+
* number, and a number without provenance is indefensible.
|
|
64
|
+
*/
|
|
65
|
+
var FpExplain = class extends BaseCommand {
|
|
66
|
+
static commandName = "fp:explain";
|
|
67
|
+
static description = "Show the provenance of a function's count";
|
|
68
|
+
static options = { startApp: false };
|
|
69
|
+
async run() {
|
|
70
|
+
this.exitCode = printResult(await runExplain({
|
|
71
|
+
root: this.app.makePath(),
|
|
72
|
+
name: this.name
|
|
73
|
+
}), printerFor(this));
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
__decorate([args.string({ description: "Function name, e.g. \"POST /books\" or \"Invite\"" })], FpExplain.prototype, "name", void 0);
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region commands/fp_diff.ts
|
|
79
|
+
/**
|
|
80
|
+
* Additions, changes and deletions between two counts — what gets invoiced.
|
|
81
|
+
*
|
|
82
|
+
* It works on a SAVED count (`fp:count --out`) compared against the current
|
|
83
|
+
* state, never on two checkouts: booting the older version, with possibly
|
|
84
|
+
* different dependencies, is a problem not worth solving.
|
|
85
|
+
*/
|
|
86
|
+
var FpDiff = class extends BaseCommand {
|
|
87
|
+
static commandName = "fp:diff";
|
|
88
|
+
static description = "Compare a saved count against the current state of the application";
|
|
89
|
+
static options = { startApp: false };
|
|
90
|
+
async run() {
|
|
91
|
+
this.exitCode = printResult(await runDiff({
|
|
92
|
+
root: this.app.makePath(),
|
|
93
|
+
previous: this.previous
|
|
94
|
+
}), printerFor(this));
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
__decorate([args.string({ description: "Path to the JSON produced by `fp:count --out`" })], FpDiff.prototype, "previous", void 0);
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region commands/fp_calibrate.ts
|
|
100
|
+
/**
|
|
101
|
+
* Measures the counter's bias against a manual count.
|
|
102
|
+
*
|
|
103
|
+
* It does NOT apply the factor: calibrating is a decision for whoever signs the
|
|
104
|
+
* contract, and a factor applied silently would stop the count from being
|
|
105
|
+
* reproducible from the code.
|
|
106
|
+
*/
|
|
107
|
+
var FpCalibrate = class extends BaseCommand {
|
|
108
|
+
static commandName = "fp:calibrate";
|
|
109
|
+
static description = "Compare the automatic count against manual counts";
|
|
110
|
+
static options = { startApp: false };
|
|
111
|
+
async run() {
|
|
112
|
+
this.exitCode = printResult(await runCalibrate({
|
|
113
|
+
root: this.app.makePath(),
|
|
114
|
+
samples: this.samples
|
|
115
|
+
}), printerFor(this));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
__decorate([args.string({ description: "CSV of `function,fp` counted by hand" })], FpCalibrate.prototype, "samples", void 0);
|
|
119
|
+
//#endregion
|
|
120
|
+
export { FpCalibrate, FpCount, FpDiff, FpExplain, FpInventory };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { Printer } from '../src/cli/print.js';
|
|
3
|
+
/**
|
|
4
|
+
* Maps a `RunResult` onto ace's logger.
|
|
5
|
+
*
|
|
6
|
+
* Notes go to `info`, not `log`: they are diagnostics about the run, and under
|
|
7
|
+
* `--json` they must not be mistaken for output.
|
|
8
|
+
*/
|
|
9
|
+
export declare function printerFor(command: BaseCommand): Printer;
|