@filipebraida/adonis-function-points 0.4.0 → 0.5.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 +53 -0
- package/README.md +17 -6
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-DLZP5bUp.js → fp_calibrate-DUbHiifm.js} +1 -1
- package/build/{fp_count-DNSwaLUD.js → fp_count-ChtblhZV.js} +1 -1
- package/build/{fp_diff-CCKxqGKh.js → fp_diff-Dt7J4IWu.js} +1 -1
- package/build/{fp_explain-Dpiby5Qx.js → fp_explain-DZJ--0-S.js} +1 -1
- package/build/{fp_inventory-DHwZzEQf.js → fp_inventory-CPtmuuke.js} +1 -1
- package/build/{fp_metrics-M84qLYaE.js → fp_metrics-et8F1Wvt.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-Dm9KvUvF.js → pipeline-CNTBhs6o.js} +167 -40
- package/build/{resolvers-vMahHkAd.js → resolvers-PJwo2Z8R.js} +46 -1
- package/build/{runners-DetZGfh5.js → runners-DIt1G85i.js} +8 -3
- package/build/src/albrecht/counter.d.ts +1 -1
- package/build/src/cli.js +2 -2
- package/build/src/inventory/graph/call_graph.d.ts +10 -0
- package/build/src/inventory/paths.d.ts +1 -0
- package/build/src/inventory/resolvers/index.d.ts +7 -0
- package/build/src/inventory/resolvers/index.js +2 -2
- package/build/src/inventory/resolvers/types.d.ts +19 -0
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,59 @@ 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.5.0
|
|
10
|
+
|
|
11
|
+
**Rule set `afp@1.4.0`.** Two classification defects are fixed and both move numbers,
|
|
12
|
+
so a 0.4.0 baseline has to be recounted.
|
|
13
|
+
|
|
14
|
+
Found by auditing a production count function by function against the code, rather than
|
|
15
|
+
by reading the report's warnings — which is where the previous rounds had been looking.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Maintenance was decided per REQUEST instead of per store.** `behavior.writes`
|
|
20
|
+
decides EI against EO and was also read as "this store is maintained", so every store
|
|
21
|
+
a writing transaction touched became an ILF. A reference table merely READ by a route
|
|
22
|
+
that writes something else counted as maintained. On a production application this
|
|
23
|
+
left exactly one EIF in the whole count, which should have been the signal.
|
|
24
|
+
- **Seeders, tests and factories counted as maintenance.** The project-wide pass read a
|
|
25
|
+
seeder's inserts as the application maintaining a table, so reference data only the
|
|
26
|
+
seed populates came out as an ILF — which the CPM does not allow. The filter on scan
|
|
27
|
+
roots drops `tests/` and `database/` only at the ROOT, and a domain-module layout puts
|
|
28
|
+
both inside `app/`. It now applies at any depth.
|
|
29
|
+
- **The override warning counted floors that were already answered**, said "one schema"
|
|
30
|
+
whatever it was given, and therefore fired on a configuration that was complete.
|
|
31
|
+
- **`opaqueReviewed` matching nothing was silent.** `detFromSchema` already warns when
|
|
32
|
+
it names a schema that is not declared; a review naming a field that does not exist
|
|
33
|
+
reviewed nothing while the warning kept firing, which reads as the tool ignoring the
|
|
34
|
+
configuration.
|
|
35
|
+
- **A review was invisible in `fp:explain`.** Its reason appeared nowhere, which defeats
|
|
36
|
+
requiring one. Reviewed floors are now marked `(opaque, reviewed)` and the reason is
|
|
37
|
+
printed — without being counted in the "Declared by override" share, since a review
|
|
38
|
+
declares no number.
|
|
39
|
+
|
|
40
|
+
### Documented
|
|
41
|
+
|
|
42
|
+
- **In CI, prefer the standalone binary.** `node ace` validates `start/env.ts` before
|
|
43
|
+
running any command, so `node ace fp:count` fails on a missing environment variable
|
|
44
|
+
that has nothing to do with counting — measured on a production application, it
|
|
45
|
+
stopped at `Missing environment variable "AUTHZ_STORE"` and never reached the
|
|
46
|
+
command. The `fp:*` commands declare `startApp: false`, which is not enough. The
|
|
47
|
+
README said the two front-ends were interchangeable; for a pipeline that only checks
|
|
48
|
+
out code, they are not.
|
|
49
|
+
|
|
50
|
+
### New
|
|
51
|
+
|
|
52
|
+
- **`CallResolver.technicalWrite()`** declares that a write is not what the transaction
|
|
53
|
+
is for. §6.5.3 reads any write as an EI, which misreads a screen that records the
|
|
54
|
+
visit; the CPM asks about primary intent. The fact is declared about the CALL, so a
|
|
55
|
+
bookkeeping helper called from several screens is declared once. It does not hide the
|
|
56
|
+
write: the store stays an ILF and stays an FTR.
|
|
57
|
+
- **`boundary.business` says when it contradicts the code.** It accepted without comment
|
|
58
|
+
a table nothing in the application writes — which is how two read-only lookup tables
|
|
59
|
+
were declared as business data on the belief they had a CRUD, when the routes were
|
|
60
|
+
`.only(['index', 'show'])`. The declaration is still honoured; the fact is reported.
|
|
61
|
+
|
|
9
62
|
## 0.4.0
|
|
10
63
|
|
|
11
64
|
**Rule set `afp@1.3.0`.** Conditional validator groups now count, and a nested
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ node ace fp:count
|
|
|
13
13
|
|
|
14
14
|
```
|
|
15
15
|
Unadjusted count: 46 FP
|
|
16
|
-
Ruleset: afp@1.
|
|
16
|
+
Ruleset: afp@1.4.0
|
|
17
17
|
|
|
18
18
|
type n FP
|
|
19
19
|
ILF 2 14
|
|
@@ -63,11 +63,21 @@ For CI, or a one-off count on a project you do not want to touch:
|
|
|
63
63
|
npx @filipebraida/adonis-function-points count --root ./my-app
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
71
81
|
|
|
72
82
|
```
|
|
73
83
|
adonis-function-points <command> [options]
|
|
@@ -75,6 +85,7 @@ adonis-function-points <command> [options]
|
|
|
75
85
|
count count the unadjusted function points
|
|
76
86
|
inventory the raw facts: stores, routes, tracing coverage
|
|
77
87
|
explain <name> why one function was counted that way
|
|
88
|
+
metrics density, coupling and conformance, from the same run
|
|
78
89
|
diff <previous.json> additions / modifications / deletions, and billable FP
|
|
79
90
|
calibrate <samples.csv> correction factors against a manual count
|
|
80
91
|
|
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-CPtmuuke.js")
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
commandName: "fp:metrics",
|
|
21
|
-
importer: () => import("../fp_metrics-
|
|
21
|
+
importer: () => import("../fp_metrics-et8F1Wvt.js")
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
commandName: "fp:count",
|
|
25
|
-
importer: () => import("../fp_count-
|
|
25
|
+
importer: () => import("../fp_count-ChtblhZV.js")
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
commandName: "fp:explain",
|
|
29
|
-
importer: () => import("../fp_explain-
|
|
29
|
+
importer: () => import("../fp_explain-DZJ--0-S.js")
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
commandName: "fp:diff",
|
|
33
|
-
importer: () => import("../fp_diff-
|
|
33
|
+
importer: () => import("../fp_diff-Dt7J4IWu.js")
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
commandName: "fp:calibrate",
|
|
37
|
-
importer: () => import("../fp_calibrate-
|
|
37
|
+
importer: () => import("../fp_calibrate-DUbHiifm.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-DIt1G85i.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-DIt1G85i.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-DIt1G85i.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-DIt1G85i.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, args } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_explain.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as runInventory, s as printResult } from "./runners-
|
|
1
|
+
import { a as runInventory, s as printResult } from "./runners-DIt1G85i.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, flags } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_inventory.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as runMetrics, s as printResult } from "./runners-
|
|
1
|
+
import { o as runMetrics, s as printResult } from "./runners-DIt1G85i.js";
|
|
2
2
|
import { n as printerFor, t as __decorate } from "./decorate-D6enDn9D.js";
|
|
3
3
|
import { BaseCommand, flags } from "@adonisjs/core/ace";
|
|
4
4
|
//#region commands/fp_metrics.ts
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { configure } from "./configure.js";
|
|
2
2
|
import { a as AEP_FACTORS, c as diffCounts, i as measureStructure, n as parseSamples, o as IncomparableRulesetsError, r as measureConformance, s as IncomparableSourcesError, t as calibrate, u as defineConfig } from "./calibration-8eV8CEix.js";
|
|
3
3
|
import "./src/types.js";
|
|
4
|
-
import { t as BUILTIN_CALL_RESOLVERS } from "./resolvers-
|
|
5
|
-
import { i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-
|
|
4
|
+
import { t as BUILTIN_CALL_RESOLVERS } from "./resolvers-PJwo2Z8R.js";
|
|
5
|
+
import { i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-CNTBhs6o.js";
|
|
6
6
|
export { AEP_FACTORS, BUILTIN_CALL_RESOLVERS, CoverageTooLowError, IncomparableRulesetsError, IncomparableSourcesError, RULESET, RULESET_VERSION, analyze, calibrate, configure, defineConfig, diffCounts, measureConformance, measureStructure, parseSamples };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as hooksFiredBy, c as isApplicationCode, d as toPosix, i as detectAccess, l as relativeTo, n as isTechnicalWrite, o as rootSymbolOf, r as resolveCall, s as collectEventBindings, t as BUILTIN_CALL_RESOLVERS, u as samePath } from "./resolvers-PJwo2Z8R.js";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
@@ -1586,12 +1586,34 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1586
1586
|
const unresolved = [];
|
|
1587
1587
|
const validator = validatorFieldsIn(body, file, app);
|
|
1588
1588
|
const request = requestFieldsIn(body);
|
|
1589
|
+
const context = {
|
|
1590
|
+
file,
|
|
1591
|
+
depth: 0,
|
|
1592
|
+
imports,
|
|
1593
|
+
exportedAs,
|
|
1594
|
+
injected,
|
|
1595
|
+
eventBindings,
|
|
1596
|
+
dataStoresBySymbol: storesByName,
|
|
1597
|
+
resolveSpecifier: app.resolveSpecifier,
|
|
1598
|
+
sourceFile
|
|
1599
|
+
};
|
|
1589
1600
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1590
1601
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
1591
1602
|
if (access) {
|
|
1603
|
+
/**
|
|
1604
|
+
* Asked about a DIRECT write too, not only about a call a resolver follows.
|
|
1605
|
+
*
|
|
1606
|
+
* `Notification.query().…update({ status: 'read' })` in a `show` handler is
|
|
1607
|
+
* exactly the shape this exists for, and the write IS the call — there is no
|
|
1608
|
+
* method to declare. Asking in one place only would have covered the service
|
|
1609
|
+
* call and missed the query builder beside it, which is the same fact written
|
|
1610
|
+
* differently.
|
|
1611
|
+
*/
|
|
1612
|
+
const technical = access.mode === "write" && isTechnicalWrite(call, context, resolvers);
|
|
1592
1613
|
accesses.push({
|
|
1593
1614
|
store: access.store,
|
|
1594
|
-
write: access.mode === "write"
|
|
1615
|
+
write: access.mode === "write",
|
|
1616
|
+
technical
|
|
1595
1617
|
});
|
|
1596
1618
|
/**
|
|
1597
1619
|
* A relation reached by `preload`/`load` is read; one written through
|
|
@@ -1619,21 +1641,18 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1619
1641
|
* shape must not be CLAIMED, not merely not reported.
|
|
1620
1642
|
*/
|
|
1621
1643
|
if (isIterationCall(call)) continue;
|
|
1622
|
-
const resolved = resolveCall(call,
|
|
1623
|
-
file,
|
|
1624
|
-
depth: 0,
|
|
1625
|
-
imports,
|
|
1626
|
-
exportedAs,
|
|
1627
|
-
injected,
|
|
1628
|
-
eventBindings,
|
|
1629
|
-
dataStoresBySymbol: storesByName,
|
|
1630
|
-
resolveSpecifier: app.resolveSpecifier,
|
|
1631
|
-
sourceFile
|
|
1632
|
-
}, resolvers);
|
|
1644
|
+
const resolved = resolveCall(call, context, resolvers);
|
|
1633
1645
|
if (resolved) {
|
|
1646
|
+
/**
|
|
1647
|
+
* Declared about the CALL, so everything reached through it is incidental too:
|
|
1648
|
+
* `persistOrganizationVisit` is called from several screens and saying it once
|
|
1649
|
+
* covers all of them.
|
|
1650
|
+
*/
|
|
1651
|
+
const technical = isTechnicalWrite(call, context, resolvers);
|
|
1634
1652
|
for (const next of resolved.refs) followUps.push({
|
|
1635
1653
|
ref: next,
|
|
1636
|
-
by: resolved.by
|
|
1654
|
+
by: resolved.by,
|
|
1655
|
+
technical
|
|
1637
1656
|
});
|
|
1638
1657
|
continue;
|
|
1639
1658
|
}
|
|
@@ -1673,6 +1692,17 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1673
1692
|
const writtenAnywhere = () => {
|
|
1674
1693
|
const written = /* @__PURE__ */ new Set();
|
|
1675
1694
|
for (const file of project.getSourceFiles()) {
|
|
1695
|
+
/**
|
|
1696
|
+
* A seeder's inserts are not the application maintaining a table, and a test
|
|
1697
|
+
* factory's are not either. Counting them made every reference table an ILF:
|
|
1698
|
+
* the CPM puts data maintained by the development team at an EIF at most, and
|
|
1699
|
+
* code data outside the count entirely.
|
|
1700
|
+
*
|
|
1701
|
+
* This is the same notion `scanRoots` applies at the root, applied at any
|
|
1702
|
+
* depth — because a domain-module layout puts `tests/` and `seeders/` inside
|
|
1703
|
+
* `app/`, where the root filter never looks.
|
|
1704
|
+
*/
|
|
1705
|
+
if (!isApplicationCode(app.root, file.getFilePath())) continue;
|
|
1676
1706
|
const symbols = storeSymbolsFor(file, file, app, storesByName);
|
|
1677
1707
|
if (symbols.size === 0) continue;
|
|
1678
1708
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
@@ -1698,6 +1728,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1698
1728
|
};
|
|
1699
1729
|
function run(handler) {
|
|
1700
1730
|
const touches = /* @__PURE__ */ new Set();
|
|
1731
|
+
const writtenStores = /* @__PURE__ */ new Set();
|
|
1701
1732
|
const inputFields = /* @__PURE__ */ new Set();
|
|
1702
1733
|
const opaqueInputFields = /* @__PURE__ */ new Set();
|
|
1703
1734
|
const requestFields = /* @__PURE__ */ new Set();
|
|
@@ -1707,7 +1738,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1707
1738
|
const unresolved = [];
|
|
1708
1739
|
const visited = /* @__PURE__ */ new Set();
|
|
1709
1740
|
let writes = false;
|
|
1710
|
-
const visit = (ref, depth) => {
|
|
1741
|
+
const visit = (ref, depth, technical = false) => {
|
|
1711
1742
|
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
|
|
1712
1743
|
if (visited.has(key) || depth > maxDepth) return;
|
|
1713
1744
|
visited.add(key);
|
|
@@ -1732,10 +1763,16 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1732
1763
|
let bodyWrites = false;
|
|
1733
1764
|
for (const access of facts.accesses) {
|
|
1734
1765
|
touches.add(access.store);
|
|
1735
|
-
if (access.write)
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1766
|
+
if (!access.write) continue;
|
|
1767
|
+
/**
|
|
1768
|
+
* The store is maintained either way — a visit table really is written by this
|
|
1769
|
+
* application, so it stays an ILF and stays an FTR. What a technical write does
|
|
1770
|
+
* not do is decide what the transaction is FOR: §6.5.3 would read a `GET` that
|
|
1771
|
+
* notes the visit as an EI, and the CPM asks about primary intent.
|
|
1772
|
+
*/
|
|
1773
|
+
writtenStores.add(access.store);
|
|
1774
|
+
bodyWrites = true;
|
|
1775
|
+
if (!technical && !access.technical) writes = true;
|
|
1739
1776
|
}
|
|
1740
1777
|
unresolved.push(...facts.unresolved);
|
|
1741
1778
|
for (const field of facts.validators) inputFields.add(field);
|
|
@@ -1757,7 +1794,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1757
1794
|
if (depth >= maxDepth) return;
|
|
1758
1795
|
for (const followUp of facts.followUps) {
|
|
1759
1796
|
const before = trace.length;
|
|
1760
|
-
visit(followUp.ref, depth + 1);
|
|
1797
|
+
visit(followUp.ref, depth + 1, technical || followUp.technical === true);
|
|
1761
1798
|
if (trace.length > before) trace[before].by = followUp.by;
|
|
1762
1799
|
}
|
|
1763
1800
|
};
|
|
@@ -1765,6 +1802,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1765
1802
|
return {
|
|
1766
1803
|
writes,
|
|
1767
1804
|
touches: [...touches].sort(),
|
|
1805
|
+
writtenStores: [...writtenStores].sort(),
|
|
1768
1806
|
inputFields: [...inputFields].sort(),
|
|
1769
1807
|
opaqueInputFields: [...opaqueInputFields].sort(),
|
|
1770
1808
|
requestFields: [...requestFields].sort(),
|
|
@@ -2348,7 +2386,7 @@ const RULESET = "afp";
|
|
|
2348
2386
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
2349
2387
|
* exists for exactly that, and only this constant arms it.
|
|
2350
2388
|
*/
|
|
2351
|
-
const RULESET_VERSION = "1.
|
|
2389
|
+
const RULESET_VERSION = "1.4.0";
|
|
2352
2390
|
function count(input, options = {}) {
|
|
2353
2391
|
const warnings = [];
|
|
2354
2392
|
const usage = usageOf(input);
|
|
@@ -2377,7 +2415,17 @@ function count(input, options = {}) {
|
|
|
2377
2415
|
* quietly counting one more store.
|
|
2378
2416
|
*/
|
|
2379
2417
|
if (business.has(store.name) || business.has(store.table ?? "")) {
|
|
2380
|
-
|
|
2418
|
+
/**
|
|
2419
|
+
* Whether any transaction WRITES it, said out loud.
|
|
2420
|
+
*
|
|
2421
|
+
* `business` is meant for data the user maintains, and it accepted without
|
|
2422
|
+
* comment a table nothing in the application writes. That is how a team put two
|
|
2423
|
+
* read-only lookup tables in it believing they had a CRUD — the routes were
|
|
2424
|
+
* `.only(['index', 'show'])`. The declaration is still honoured, because only a
|
|
2425
|
+
* person knows, but the fact that contradicts it is now in the report.
|
|
2426
|
+
*/
|
|
2427
|
+
const maintained = usage.get(store.name)?.written === true || (input.writtenAnywhere?.has(store.name) ?? false);
|
|
2428
|
+
warnings.push(`kept by boundary configuration: ${store.name} — the AFP naming filter had excluded it (${technical})` + (maintained ? "" : ". NOTE: no transaction of this application writes it, so it counts as an EIF — check that the screens for it are more than index and show"));
|
|
2381
2429
|
return true;
|
|
2382
2430
|
}
|
|
2383
2431
|
warnings.push(`technical, excluded: ${store.name} (${technical})`);
|
|
@@ -2410,7 +2458,7 @@ function count(input, options = {}) {
|
|
|
2410
2458
|
* reviewed is still printed.
|
|
2411
2459
|
*/
|
|
2412
2460
|
const reviewed = reviewedOpaque(options.overrides ?? {});
|
|
2413
|
-
const functions = applyOverrides([...dataFunctions, ...transactionalFunctions], options.overrides ?? {}, input.jsonSchemas ?? /* @__PURE__ */ new Map(), tables, weights, warnings);
|
|
2461
|
+
const functions = applyOverrides([...dataFunctions, ...transactionalFunctions], options.overrides ?? {}, input.jsonSchemas ?? /* @__PURE__ */ new Map(), tables, weights, warnings, reviewed);
|
|
2414
2462
|
/**
|
|
2415
2463
|
* Reported AFTER the overrides are applied, because the overrides are the answer
|
|
2416
2464
|
* to it.
|
|
@@ -2423,7 +2471,8 @@ function count(input, options = {}) {
|
|
|
2423
2471
|
const declared = new Set(functions.filter((fn) => fn.rationale.overrides?.some((o) => o.fields.includes("det"))).map((fn) => fn.name));
|
|
2424
2472
|
warnings.push(...opaqueWarnings(countable, input, {
|
|
2425
2473
|
reviewed,
|
|
2426
|
-
declared
|
|
2474
|
+
declared,
|
|
2475
|
+
overrides: options.overrides ?? {}
|
|
2427
2476
|
}));
|
|
2428
2477
|
warnings.push(...unreadableInputWarnings(input));
|
|
2429
2478
|
return {
|
|
@@ -2458,9 +2507,35 @@ function reviewedOpaque(overrides) {
|
|
|
2458
2507
|
for (const [name, override] of Object.entries(overrides)) for (const entry of override.opaqueReviewed ?? []) {
|
|
2459
2508
|
reviewed.add(entry);
|
|
2460
2509
|
reviewed.add(`${name}.${entry}`);
|
|
2510
|
+
/**
|
|
2511
|
+
* The bare field too, because the two sides of this comparison spell things
|
|
2512
|
+
* differently. `opaqueReviewed` is written against the FUNCTION (`Petition.schema`)
|
|
2513
|
+
* while a rationale source carries the TABLE (`ast:petitions.schema`), and the
|
|
2514
|
+
* qualified form cannot be recovered from either. The last segment is what they
|
|
2515
|
+
* share, and without it a correctly written review matched the count and not the
|
|
2516
|
+
* rationale — so `fp:explain` showed no review and the override warning still
|
|
2517
|
+
* claimed five unanswered floors.
|
|
2518
|
+
*/
|
|
2519
|
+
reviewed.add(entry.split(".").pop() ?? entry);
|
|
2461
2520
|
}
|
|
2462
2521
|
return reviewed;
|
|
2463
2522
|
}
|
|
2523
|
+
/**
|
|
2524
|
+
* The identity of an opaque DET inside a rationale source.
|
|
2525
|
+
*
|
|
2526
|
+
* `ast:petitions.schema (opaque)` is the store's TABLE name, and `opaqueReviewed` is
|
|
2527
|
+
* written against the FUNCTION name (`Petition.schema`), so the qualified form cannot
|
|
2528
|
+
* be recovered from the source alone — the bare field is what both sides share.
|
|
2529
|
+
*/
|
|
2530
|
+
const opaqueNameOf = (source) => source.replace(/^[a-z-]+:/, "").replace(/ \(opaque.*\)$/, "");
|
|
2531
|
+
const shortOpaqueNameOf = (source) => opaqueNameOf(source).split(".").pop() ?? "";
|
|
2532
|
+
/** `fp:explain` should say which floors someone has already looked at */
|
|
2533
|
+
function markReviewed(sources, reviewed) {
|
|
2534
|
+
return sources.map((source) => {
|
|
2535
|
+
if (!source.endsWith("(opaque)")) return source;
|
|
2536
|
+
return reviewed.has(opaqueNameOf(source)) || reviewed.has(shortOpaqueNameOf(source)) ? source.replace("(opaque)", "(opaque, reviewed)") : source;
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2464
2539
|
/** a column whose shape says nothing about what it holds */
|
|
2465
2540
|
const OPAQUE_TYPE = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
2466
2541
|
/**
|
|
@@ -2491,6 +2566,7 @@ function opaqueWarnings(stores, input, state) {
|
|
|
2491
2566
|
kind,
|
|
2492
2567
|
floor: [],
|
|
2493
2568
|
reviewed: 0,
|
|
2569
|
+
reviewedNames: [],
|
|
2494
2570
|
transactions
|
|
2495
2571
|
};
|
|
2496
2572
|
byFunction.set(name, found);
|
|
@@ -2501,15 +2577,40 @@ function opaqueWarnings(stores, input, state) {
|
|
|
2501
2577
|
for (const attribute of store.attributes) {
|
|
2502
2578
|
if (!attribute.type || !OPAQUE_TYPE.test(attribute.type)) continue;
|
|
2503
2579
|
const entry = tally(store.name, "column", reached.get(store.name) ?? 0);
|
|
2504
|
-
if (state.reviewed.has(`${store.name}.${attribute.name}`) || state.reviewed.has(attribute.name))
|
|
2505
|
-
|
|
2580
|
+
if (state.reviewed.has(`${store.name}.${attribute.name}`) || state.reviewed.has(attribute.name)) {
|
|
2581
|
+
entry.reviewed += 1;
|
|
2582
|
+
entry.reviewedNames.push(attribute.name);
|
|
2583
|
+
} else entry.floor.push(`${attribute.name} (${attribute.type})`);
|
|
2506
2584
|
}
|
|
2507
2585
|
}
|
|
2508
2586
|
for (const point of input.entryPoints) for (const field of input.behaviors.get(point.id)?.opaqueInputFields ?? []) {
|
|
2509
2587
|
const entry = tally(point.identity, "input object", 1);
|
|
2510
|
-
if (state.reviewed.has(field) || state.reviewed.has(`${point.identity}.${field}`))
|
|
2511
|
-
|
|
2588
|
+
if (state.reviewed.has(field) || state.reviewed.has(`${point.identity}.${field}`)) {
|
|
2589
|
+
entry.reviewed += 1;
|
|
2590
|
+
entry.reviewedNames.push(field);
|
|
2591
|
+
} else entry.floor.push(field);
|
|
2512
2592
|
}
|
|
2593
|
+
/**
|
|
2594
|
+
* A review that matches nothing is a review that does nothing.
|
|
2595
|
+
*
|
|
2596
|
+
* `detFromSchema` already warns when it names a schema that is not declared, and
|
|
2597
|
+
* `opaqueReviewed` did not — so `['messages.schema']` against a field actually named
|
|
2598
|
+
* `createMessageValidator.messages.schema` reviewed nothing in silence while the
|
|
2599
|
+
* warning kept firing, which reads as the tool ignoring the configuration.
|
|
2600
|
+
*/
|
|
2601
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2602
|
+
for (const [name, entry] of byFunction) for (const field of [...entry.floor, ...entry.reviewedNames]) {
|
|
2603
|
+
const bare = field.replace(/ \(.*\)$/, "");
|
|
2604
|
+
seen.add(bare);
|
|
2605
|
+
seen.add(`${name}.${bare}`);
|
|
2606
|
+
seen.add(bare.split(".").pop() ?? bare);
|
|
2607
|
+
}
|
|
2608
|
+
const unmatched = [];
|
|
2609
|
+
for (const [name, override] of Object.entries(state.overrides)) for (const declaredName of override.opaqueReviewed ?? []) if (![
|
|
2610
|
+
declaredName,
|
|
2611
|
+
`${name}.${declaredName}`,
|
|
2612
|
+
declaredName.split(".").pop() ?? ""
|
|
2613
|
+
].some((spelling) => seen.has(spelling))) unmatched.push(`${name}.opaqueReviewed: ${declaredName}`);
|
|
2513
2614
|
const lines = [];
|
|
2514
2615
|
let answered = 0;
|
|
2515
2616
|
for (const [name, entry] of byFunction) {
|
|
@@ -2529,11 +2630,13 @@ function opaqueWarnings(stores, input, state) {
|
|
|
2529
2630
|
lines.push(` ${name} — ${remaining.length} ${entry.kind}(s) at 1 DET${reach}` + (answeredHere.length > 0 ? ` (${answeredHere.join(", ")} already)` : "") + `: ${remaining.map((f) => entry.kind === "column" ? `${name}.${f}` : f).join(", ")}`);
|
|
2530
2631
|
}
|
|
2531
2632
|
const settled = answered === 0 ? [] : [` (${answered} more function(s) whose opaque DETs are all accounted for)`];
|
|
2532
|
-
|
|
2633
|
+
const unmatchedLines = unmatched.length === 0 ? [] : [`${unmatched.length} \`opaqueReviewed\` entr(ies) match no opaque DET, so they review nothing. The name is the one the count prints:`, ...unmatched.map((u) => ` ${u}`)];
|
|
2634
|
+
if (lines.length === 0) return [...unmatchedLines, ...settled];
|
|
2533
2635
|
return [
|
|
2534
2636
|
`${lines.length} function(s) with a DET the analysis cannot read, counted as 1 each — a FLOOR, not a measurement. Where the fields are declared in the source, name that schema with \`overrides.detFromSchema\`; where 1 is the right answer, record it with \`overrides.<fn>.opaqueReviewed\` — counting-decisions §8:`,
|
|
2535
2637
|
...lines,
|
|
2536
|
-
...settled
|
|
2638
|
+
...settled,
|
|
2639
|
+
...unmatchedLines
|
|
2537
2640
|
];
|
|
2538
2641
|
}
|
|
2539
2642
|
/**
|
|
@@ -2583,7 +2686,7 @@ function unreadableInputWarnings(input) {
|
|
|
2583
2686
|
* An override naming no function is a warning, never silence: a typo in the key
|
|
2584
2687
|
* would otherwise mean the declaration did nothing and nobody was told.
|
|
2585
2688
|
*/
|
|
2586
|
-
function applyOverrides(functions, overrides, schemas, tables, weights, warnings) {
|
|
2689
|
+
function applyOverrides(functions, overrides, schemas, tables, weights, warnings, reviewed) {
|
|
2587
2690
|
const keys = Object.keys(overrides);
|
|
2588
2691
|
if (keys.length === 0) return functions;
|
|
2589
2692
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -2639,21 +2742,34 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2639
2742
|
det = Math.max(fn.det - Math.min(placeholders.length, 1), 0) + schema.fields;
|
|
2640
2743
|
by = `config:overrides.${fn.name} (from ${schema.name}: ${schema.fields} fields)`;
|
|
2641
2744
|
if (placeholders.length === 0) warnings.push(`override for "${fn.name}" names schema "${schema.name}", but this function has no opaque DET for it to stand in for: the ${schema.fields} fields were ADDED to the ${fn.det} already counted. Check the override is on the right function.`);
|
|
2642
|
-
else
|
|
2745
|
+
else {
|
|
2746
|
+
/**
|
|
2747
|
+
* Only the placeholders nobody has answered are worth reporting.
|
|
2748
|
+
*
|
|
2749
|
+
* The message used to count every opaque DET of the function and say "names
|
|
2750
|
+
* one schema" whatever it was given. With four of five columns in
|
|
2751
|
+
* `opaqueReviewed` and a LIST of two schemas, it still fired, still said
|
|
2752
|
+
* "one schema", and still counted the four already answered — a warning
|
|
2753
|
+
* wrong on all three counts, about a configuration that was complete.
|
|
2754
|
+
*/
|
|
2755
|
+
const unanswered = placeholders.filter((source) => !reviewed.has(opaqueNameOf(source)) && !reviewed.has(shortOpaqueNameOf(source)));
|
|
2756
|
+
if (unanswered.length > 1) warnings.push(`override for "${fn.name}" names ${named.length === 1 ? "one schema" : `${named.length} schemas`} and the function has ${unanswered.length} unanswered opaque DETs (${unanswered.join(", ")}). One was replaced; the others still count 1 each — declare them or record them with \`opaqueReviewed\`.`);
|
|
2757
|
+
}
|
|
2643
2758
|
}
|
|
2644
2759
|
}
|
|
2645
2760
|
const refs = override.refs ?? fn.refs;
|
|
2646
2761
|
const complexity = complexityOf(fn.type, refs, det, tables);
|
|
2647
2762
|
/**
|
|
2648
|
-
*
|
|
2763
|
+
* A review is recorded with NO fields, and the reporter's "declared by override"
|
|
2764
|
+
* share counts only entries that declared one.
|
|
2649
2765
|
*
|
|
2650
|
-
*
|
|
2651
|
-
*
|
|
2652
|
-
*
|
|
2653
|
-
* total declared by override"
|
|
2654
|
-
*
|
|
2766
|
+
* Dropping it entirely lost the `reason`, so an `opaqueReviewed`-only decision
|
|
2767
|
+
* appeared nowhere — not in `fp:explain`, not anywhere — which defeats the point
|
|
2768
|
+
* of requiring a reason. Counting it in the share was the opposite error: it read
|
|
2769
|
+
* as "1 function, 7 FP, 35% of the total declared by override" when no number had
|
|
2770
|
+
* been declared at all.
|
|
2655
2771
|
*/
|
|
2656
|
-
|
|
2772
|
+
const marked = markReviewed(fn.rationale.detSources, reviewed);
|
|
2657
2773
|
return {
|
|
2658
2774
|
...fn,
|
|
2659
2775
|
det,
|
|
@@ -2662,6 +2778,7 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2662
2778
|
points: pointsOf(fn.type, complexity, weights),
|
|
2663
2779
|
rationale: {
|
|
2664
2780
|
...fn.rationale,
|
|
2781
|
+
detSources: marked,
|
|
2665
2782
|
overrides: [...fn.rationale.overrides ?? [], {
|
|
2666
2783
|
by,
|
|
2667
2784
|
reason: override.reason,
|
|
@@ -2690,7 +2807,16 @@ function usageOf(input) {
|
|
|
2690
2807
|
};
|
|
2691
2808
|
usage.set(store, {
|
|
2692
2809
|
used: true,
|
|
2693
|
-
|
|
2810
|
+
/**
|
|
2811
|
+
* Per STORE, not per transaction.
|
|
2812
|
+
*
|
|
2813
|
+
* `behavior.writes` decides EI against EO and says nothing about which of
|
|
2814
|
+
* the tables was written. Read as "this store is maintained", a reference
|
|
2815
|
+
* table merely READ by a route that writes something else became an ILF —
|
|
2816
|
+
* and on a production application that left exactly one EIF in the whole
|
|
2817
|
+
* count, which should have been the signal.
|
|
2818
|
+
*/
|
|
2819
|
+
written: current.written || behavior.writtenStores.includes(store)
|
|
2694
2820
|
});
|
|
2695
2821
|
}
|
|
2696
2822
|
}
|
|
@@ -2880,6 +3006,7 @@ async function analyze(root, options = {}) {
|
|
|
2880
3006
|
entryPointId,
|
|
2881
3007
|
writes: behavior.writes,
|
|
2882
3008
|
touches: behavior.touches,
|
|
3009
|
+
writtenStores: behavior.writtenStores,
|
|
2883
3010
|
inputFields: behavior.inputFields.map((name) => ({
|
|
2884
3011
|
name,
|
|
2885
3012
|
provenance: {
|
|
@@ -40,6 +40,42 @@ const toPosix = (value) => value.split("\\").join("/");
|
|
|
40
40
|
const relativeTo = (root, value) => toPosix(path.relative(toPosix(root), toPosix(value))) || ".";
|
|
41
41
|
/** Compares two paths that may have come from different sources. */
|
|
42
42
|
const samePath = (a, b) => a !== void 0 && b !== void 0 && toPosix(a) === toPosix(b);
|
|
43
|
+
/**
|
|
44
|
+
* Is this file the application's own code, as opposed to the scaffolding around it?
|
|
45
|
+
*
|
|
46
|
+
* The top-level filter on `scanRoots` already drops `tests/`, `database/` and the
|
|
47
|
+
* rest — but only at the ROOT. Applications organised by domain module put both
|
|
48
|
+
* inside `app/`:
|
|
49
|
+
*
|
|
50
|
+
* app/billing/tests/functional/invoice.spec.ts
|
|
51
|
+
* app/billing/seeders/plan_seeder.ts
|
|
52
|
+
*
|
|
53
|
+
* so they land in the project, and `writtenAnywhere()` read a seeder's inserts as
|
|
54
|
+
* the application maintaining the table. A reference table only the seed populates
|
|
55
|
+
* came out as an ILF — which the CPM does not allow: data maintained by the
|
|
56
|
+
* development team is at most an EIF, and code data is not counted at all.
|
|
57
|
+
*
|
|
58
|
+
* The segments are AdonisJS's own: `make:test` writes to a suite directory,
|
|
59
|
+
* `make:seeder` to `seeders`, `make:migration` to `migrations`, `make:factory` to
|
|
60
|
+
* `factories`. The `.spec`/`.test` suffixes come from the suite globs in
|
|
61
|
+
* `adonisrc.ts`.
|
|
62
|
+
*/
|
|
63
|
+
const SCAFFOLDING = new Set([
|
|
64
|
+
"tests",
|
|
65
|
+
"test",
|
|
66
|
+
"seeders",
|
|
67
|
+
"seeder",
|
|
68
|
+
"migrations",
|
|
69
|
+
"factories"
|
|
70
|
+
]);
|
|
71
|
+
function isApplicationCode(root, file) {
|
|
72
|
+
const relative = relativeTo(root, file);
|
|
73
|
+
if (relative.startsWith("..")) return false;
|
|
74
|
+
const parts = relative.split("/");
|
|
75
|
+
const name = parts.at(-1) ?? "";
|
|
76
|
+
if (/\.(spec|test)\.[jt]s$/.test(name)) return false;
|
|
77
|
+
return !parts.slice(0, -1).some((segment) => SCAFFOLDING.has(segment));
|
|
78
|
+
}
|
|
43
79
|
//#endregion
|
|
44
80
|
//#region src/inventory/sources/event_bindings.ts
|
|
45
81
|
/** the method a listener declares; AdonisJS calls `handle` unless told otherwise */
|
|
@@ -817,6 +853,15 @@ const BUILTIN_CALL_RESOLVERS = [
|
|
|
817
853
|
* them apart. Hence specific strategies declare a lower `order` than generic
|
|
818
854
|
* ones, and `module-function` comes last — it would match almost anything.
|
|
819
855
|
*/
|
|
856
|
+
/**
|
|
857
|
+
* Does any strategy call this a technical write?
|
|
858
|
+
*
|
|
859
|
+
* Asked separately from resolution, because the strategy that recognises the call as
|
|
860
|
+
* incidental is not necessarily the one that knows where it goes.
|
|
861
|
+
*/
|
|
862
|
+
function isTechnicalWrite(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
863
|
+
return resolvers.some((resolver) => resolver.technicalWrite?.(call, ctx) === true);
|
|
864
|
+
}
|
|
820
865
|
function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
821
866
|
for (const resolver of resolvers) {
|
|
822
867
|
/**
|
|
@@ -837,4 +882,4 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
|
837
882
|
return null;
|
|
838
883
|
}
|
|
839
884
|
//#endregion
|
|
840
|
-
export {
|
|
885
|
+
export { hooksFiredBy as a, isApplicationCode as c, toPosix as d, detectAccess as i, relativeTo as l, isTechnicalWrite as n, rootSymbolOf as o, resolveCall as r, collectEventBindings as s, BUILTIN_CALL_RESOLVERS as t, samePath as u };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as diffCounts, i as measureStructure, l as DEFAULTS, n as parseSamples, o as IncomparableRulesetsError, r as measureConformance, s as IncomparableSourcesError, t as calibrate, u as defineConfig } from "./calibration-8eV8CEix.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as analyze } from "./pipeline-
|
|
2
|
+
import { d as toPosix } from "./resolvers-PJwo2Z8R.js";
|
|
3
|
+
import { n as analyze } from "./pipeline-CNTBhs6o.js";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -91,7 +91,12 @@ function renderCount(result) {
|
|
|
91
91
|
* habit: if it grows, the count comes from a spreadsheet and the tool loses
|
|
92
92
|
* its reason to exist. Printing the share is what keeps that visible.
|
|
93
93
|
*/
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Only entries that DECLARED a number. A review records a decision and declares
|
|
96
|
+
* nothing, so counting it here would read as "35% of the total declared by
|
|
97
|
+
* override" about a count nobody touched.
|
|
98
|
+
*/
|
|
99
|
+
const overridden = result.functions.filter((fn) => fn.rationale.overrides?.some((o) => o.fields.length > 0));
|
|
95
100
|
if (overridden.length > 0) {
|
|
96
101
|
const points = overridden.reduce((total, fn) => total + fn.points, 0);
|
|
97
102
|
const share = (points / (result.totals.unadjusted || 1) * 100).toFixed(1);
|
|
@@ -34,7 +34,7 @@ export declare const RULESET = "afp";
|
|
|
34
34
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
35
35
|
* exists for exactly that, and only this constant arms it.
|
|
36
36
|
*/
|
|
37
|
-
export declare const RULESET_VERSION = "1.
|
|
37
|
+
export declare const RULESET_VERSION = "1.4.0";
|
|
38
38
|
export type CountInput = {
|
|
39
39
|
app: AppContext;
|
|
40
40
|
stores: CollectedDataStore[];
|
package/build/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CoverageTooLowError } from "../pipeline-
|
|
2
|
-
import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-
|
|
1
|
+
import { t as CoverageTooLowError } from "../pipeline-CNTBhs6o.js";
|
|
2
|
+
import { a as runInventory, c as ConfigLoadError, i as runExplain, n as runCount, o as runMetrics, r as runDiff, s as printResult, t as runCalibrate } from "../runners-DIt1G85i.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -30,6 +30,16 @@ export type Behavior = {
|
|
|
30
30
|
writes: boolean;
|
|
31
31
|
/** data stores reached */
|
|
32
32
|
touches: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Of those, the ones this transaction WRITES.
|
|
35
|
+
*
|
|
36
|
+
* `writes` is a property of the transaction — it decides EI against EO — and was
|
|
37
|
+
* being read as a property of every store the transaction touched: a table merely
|
|
38
|
+
* read by a route that writes something else counted as maintained, so almost
|
|
39
|
+
* nothing could be an EIF. §6.5.4 asks who maintains THIS store, which is a
|
|
40
|
+
* question about the access, not about the request.
|
|
41
|
+
*/
|
|
42
|
+
writtenStores: string[];
|
|
33
43
|
/**
|
|
34
44
|
* Declared input fields: `request.validateUsing(x)` resolved down to the
|
|
35
45
|
* fields of the VineJS schema — counting-decisions §7.
|
|
@@ -37,3 +37,4 @@ export declare const toPosix: (value: string) => string;
|
|
|
37
37
|
export declare const relativeTo: (root: string, value: string) => string;
|
|
38
38
|
/** Compares two paths that may have come from different sources. */
|
|
39
39
|
export declare const samePath: (a: string | undefined, b: string | undefined) => boolean;
|
|
40
|
+
export declare function isApplicationCode(root: string, file: string): boolean;
|
|
@@ -17,6 +17,13 @@ export declare const BUILTIN_CALL_RESOLVERS: CallResolver[];
|
|
|
17
17
|
* them apart. Hence specific strategies declare a lower `order` than generic
|
|
18
18
|
* ones, and `module-function` comes last — it would match almost anything.
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Does any strategy call this a technical write?
|
|
22
|
+
*
|
|
23
|
+
* Asked separately from resolution, because the strategy that recognises the call as
|
|
24
|
+
* incidental is not necessarily the one that knows where it goes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isTechnicalWrite(call: import('ts-morph').CallExpression, ctx: import('./types.js').ResolverContext, resolvers?: CallResolver[]): boolean;
|
|
20
27
|
export declare function resolveCall(call: import('ts-morph').CallExpression, ctx: import('./types.js').ResolverContext, resolvers?: CallResolver[]): {
|
|
21
28
|
by: string;
|
|
22
29
|
refs: import('../../types.js').HandlerRef[];
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-
|
|
2
|
-
export { BUILTIN_CALL_RESOLVERS, resolveCall };
|
|
1
|
+
import { n as isTechnicalWrite, r as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-PJwo2Z8R.js";
|
|
2
|
+
export { BUILTIN_CALL_RESOLVERS, isTechnicalWrite, resolveCall };
|
|
@@ -97,4 +97,23 @@ export interface CallResolver {
|
|
|
97
97
|
* defect this package can have, whoever writes it.
|
|
98
98
|
*/
|
|
99
99
|
ignores?(call: CallExpression, ctx: ResolverContext): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* "This call writes, and the write is not what the transaction is FOR."
|
|
102
|
+
*
|
|
103
|
+
* AFP §6.5.3 decides EI against EO mechanically: a transaction that modifies a data
|
|
104
|
+
* store is an EI. That is deliberate — repeatability over CPM fidelity — and it
|
|
105
|
+
* misreads one shape: a screen that records a visit, a last-seen organisation, a
|
|
106
|
+
* view counter. The CPM asks what the elementary process is PRIMARILY for, and for a
|
|
107
|
+
* `GET` that shows a record while noting the visit, the answer is presentation.
|
|
108
|
+
*
|
|
109
|
+
* So the fact is declared about the CALL, not about each transaction that reaches it:
|
|
110
|
+
* `persistOrganizationVisit` is called from several screens and saying it once covers
|
|
111
|
+
* all of them.
|
|
112
|
+
*
|
|
113
|
+
* It does NOT hide the write. The store is still maintained by this application —
|
|
114
|
+
* still an ILF, still an FTR of the transaction — and only the transaction's
|
|
115
|
+
* classification changes. A resolver that wanted the write to disappear would use
|
|
116
|
+
* `ignores`, and would be wrong to.
|
|
117
|
+
*/
|
|
118
|
+
technicalWrite?(call: CallExpression, ctx: ResolverContext): boolean;
|
|
100
119
|
}
|
package/build/src/pipeline.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as analyze, t as CoverageTooLowError } from "../pipeline-
|
|
1
|
+
import { n as analyze, t as CoverageTooLowError } from "../pipeline-CNTBhs6o.js";
|
|
2
2
|
export { CoverageTooLowError, analyze };
|
package/build/src/types.d.ts
CHANGED
|
@@ -84,6 +84,8 @@ export type HandlerBehavior = {
|
|
|
84
84
|
writes: boolean;
|
|
85
85
|
/** DataStores reached (ids) */
|
|
86
86
|
touches: string[];
|
|
87
|
+
/** of those, the ones this transaction writes — §6.5.4 is per store, not per request */
|
|
88
|
+
writtenStores: string[];
|
|
87
89
|
/** declared input fields (validators) */
|
|
88
90
|
inputFields: Field[];
|
|
89
91
|
/**
|