@filipebraida/adonis-function-points 0.3.0 → 0.4.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 +60 -0
- package/README.md +1 -1
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-iFAec0tA.js → fp_calibrate-DLZP5bUp.js} +1 -1
- package/build/{fp_count-D21tQ_pv.js → fp_count-DNSwaLUD.js} +1 -1
- package/build/{fp_diff-D0pHGMgi.js → fp_diff-CCKxqGKh.js} +1 -1
- package/build/{fp_explain-BwFs-LW-.js → fp_explain-Dpiby5Qx.js} +1 -1
- package/build/{fp_inventory-Bu6O1Nn0.js → fp_inventory-DHwZzEQf.js} +1 -1
- package/build/{fp_metrics-MGDppfSa.js → fp_metrics-M84qLYaE.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-CIAydCcT.js → pipeline-Dm9KvUvF.js} +292 -56
- package/build/{resolvers-CRB6lXoo.js → resolvers-vMahHkAd.js} +19 -1
- package/build/{runners-CmxNHuuq.js → runners-DetZGfh5.js} +13 -3
- package/build/src/albrecht/counter.d.ts +1 -1
- package/build/src/albrecht/transactional_functions.d.ts +9 -0
- package/build/src/cli.js +2 -2
- package/build/src/define_config.d.ts +26 -1
- package/build/src/inventory/paths.d.ts +17 -0
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/inventory/source.d.ts +8 -1
- package/build/src/pipeline.js +1 -1
- package/build/stubs/config.stub +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,66 @@ 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.4.0
|
|
10
|
+
|
|
11
|
+
**Rule set `afp@1.3.0`.** Conditional validator groups now count, and a nested
|
|
12
|
+
schema is recognised however the formatter wrapped it — so a 0.3.0 baseline has to
|
|
13
|
+
be recounted.
|
|
14
|
+
|
|
15
|
+
Five items reported from real use of 0.3.0, three of them defects.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **A newline decided whether a field counted.** The recogniser for a nested schema
|
|
20
|
+
was a regex over the property's source text (`/vine\.object/`), and Prettier breaks
|
|
21
|
+
a long chain across lines — `data: vine` then `.object({})` — so the regex missed
|
|
22
|
+
and the field was counted as one leaf instead of its nested ones, and never marked
|
|
23
|
+
opaque. Decided by structure now: is this literal the argument of a call named
|
|
24
|
+
`object`? The same applied to `.merge(…)` and `group.if(…)`, which had the same
|
|
25
|
+
kind of check. A count that depends on where the formatter put a newline is not a
|
|
26
|
+
measurement — the reason the implementation-scope hash strips whitespace before
|
|
27
|
+
hashing.
|
|
28
|
+
- **The list of unreadable DETs ignored the overrides that answered it.** It was
|
|
29
|
+
computed before `applyOverrides` ran, so a function whose floor `detFromSchema` had
|
|
30
|
+
already replaced still appeared under "this is a FLOOR", telling the reader to map
|
|
31
|
+
something already mapped. It caused a real misreading of a production report, by
|
|
32
|
+
the author of this code. It now runs after the overrides, is grouped by FUNCTION,
|
|
33
|
+
and states per function how many were replaced by an override, how many reviewed,
|
|
34
|
+
and how many are still unanswered — only the last being a request to do anything.
|
|
35
|
+
- **Emitted artefacts carried absolute paths.** `CountSource.app` is documented as
|
|
36
|
+
never being one, because that says where the machine keeps its files and travels
|
|
37
|
+
with every artefact sent anywhere — and the rule was applied to that one field. A
|
|
38
|
+
production count carried **858** absolute paths in its traces; an inventory
|
|
39
|
+
carried **2036** across ten fields, including the data-store `id`. Every path that
|
|
40
|
+
leaves is now relative to the application root, and the internal absolute form is
|
|
41
|
+
untouched because that is what ts-morph resolves against.
|
|
42
|
+
- **`vine.group` and `.merge()` were not read.**
|
|
43
|
+
`vine.object({}).merge(vine.group([vine.group.if(p, {…})]))` reported the whole
|
|
44
|
+
validator as an open input object: five fields counted as one, and the report said
|
|
45
|
+
they were data when they are in the code. The branches are mutually exclusive at
|
|
46
|
+
runtime and the transaction can carry any of them, so §7.2 counts their union — a
|
|
47
|
+
field two branches share counts once. The group usually lives in an unexported
|
|
48
|
+
constant beside the validator, so the reference is resolved in the validator's own
|
|
49
|
+
file rather than the caller's.
|
|
50
|
+
- **`fp:explain` matched by substring even when the exact name existed.** Asking
|
|
51
|
+
about `POST /orders/:param/submit` returned four functions, because
|
|
52
|
+
`/submit-ready` and `/submit-ready/return` contain it. An exact name now wins
|
|
53
|
+
outright; the substring search is the fallback.
|
|
54
|
+
|
|
55
|
+
### New
|
|
56
|
+
|
|
57
|
+
- **`detFromSchema` accepts a list**, unioned by leaf path. An ILF's DETs are the
|
|
58
|
+
fields the user recognises in the file, and an application with one schema per
|
|
59
|
+
template recognises all of them. Pointing at the largest and justifying it in
|
|
60
|
+
`reason` gives the same answer only while they land in the same band — reasoning
|
|
61
|
+
the configuration should not have to carry.
|
|
62
|
+
- **`overrides.<fn>.opaqueReviewed`** records that someone looked at an opaque DET
|
|
63
|
+
and decided 1 is right. 1 DET is a floor and `fp:count` says so on every run, but
|
|
64
|
+
some of those columns really are one field, and a warning that cannot be answered
|
|
65
|
+
is one the team learns to scroll past. It moves no number, it is not counted as a
|
|
66
|
+
declared override in the "Declared by override" share, and the volume reviewed is
|
|
67
|
+
still printed.
|
|
68
|
+
|
|
9
69
|
## 0.3.0
|
|
10
70
|
|
|
11
71
|
**Rule set `afp@1.2.0`.** Three counting fixes move the number for unchanged code,
|
package/README.md
CHANGED
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-DHwZzEQf.js")
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
commandName: "fp:metrics",
|
|
21
|
-
importer: () => import("../fp_metrics-
|
|
21
|
+
importer: () => import("../fp_metrics-M84qLYaE.js")
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
commandName: "fp:count",
|
|
25
|
-
importer: () => import("../fp_count-
|
|
25
|
+
importer: () => import("../fp_count-DNSwaLUD.js")
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
commandName: "fp:explain",
|
|
29
|
-
importer: () => import("../fp_explain-
|
|
29
|
+
importer: () => import("../fp_explain-Dpiby5Qx.js")
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
commandName: "fp:diff",
|
|
33
|
-
importer: () => import("../fp_diff-
|
|
33
|
+
importer: () => import("../fp_diff-CCKxqGKh.js")
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
commandName: "fp:calibrate",
|
|
37
|
-
importer: () => import("../fp_calibrate-
|
|
37
|
+
importer: () => import("../fp_calibrate-DLZP5bUp.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-DetZGfh5.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-DetZGfh5.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-DetZGfh5.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-DetZGfh5.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-DetZGfh5.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-DetZGfh5.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-vMahHkAd.js";
|
|
5
|
+
import { i as RULESET_VERSION, n as analyze, r as RULESET, t as CoverageTooLowError } from "./pipeline-Dm9KvUvF.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 rootSymbolOf, c as
|
|
1
|
+
import { a as rootSymbolOf, c as samePath, i as hooksFiredBy, l as toPosix, n as resolveCall, o as collectEventBindings, r as detectAccess, s as relativeTo, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-vMahHkAd.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";
|
|
@@ -1248,7 +1248,14 @@ function validatorFieldsIn(body, file, app) {
|
|
|
1248
1248
|
const name = argument.getText();
|
|
1249
1249
|
const declaration = findValidator(name, file, app);
|
|
1250
1250
|
if (!declaration) continue;
|
|
1251
|
-
|
|
1251
|
+
/**
|
|
1252
|
+
* The reference is resolved in the VALIDATOR's file, not the caller's.
|
|
1253
|
+
*
|
|
1254
|
+
* `vine.object({}).merge(openaiOrAws)` names a constant that lives beside the
|
|
1255
|
+
* validator and is usually not exported, so looking for it where the call site
|
|
1256
|
+
* is finds nothing — which is how five fields stayed invisible.
|
|
1257
|
+
*/
|
|
1258
|
+
const { leaves, opaque: unreadable } = leavesOf(declaration, (ref) => findValidator(ref, declaration.getSourceFile(), app));
|
|
1252
1259
|
const isOpaque = new Set(unreadable);
|
|
1253
1260
|
for (const leaf of leaves) {
|
|
1254
1261
|
const field = `${name}.${leaf}`;
|
|
@@ -1344,7 +1351,54 @@ function findValidator(name, file, app) {
|
|
|
1344
1351
|
}
|
|
1345
1352
|
return null;
|
|
1346
1353
|
}
|
|
1347
|
-
|
|
1354
|
+
/**
|
|
1355
|
+
* Leaves of a VineJS schema, per the table in §7, and which of them are opaque.
|
|
1356
|
+
*
|
|
1357
|
+
* `answers: vine.object({}).allowUnknownProperties()` declares a field whose own
|
|
1358
|
+
* fields live in data, not in code. Walking into the empty literal found nothing
|
|
1359
|
+
* and then never pushed `answers` either, so the field counted ZERO — while an
|
|
1360
|
+
* opaque JSON column in the same position counts 1. The two are the same
|
|
1361
|
+
* situation and now get the same answer: one DET, and a warning that says the
|
|
1362
|
+
* number is a floor.
|
|
1363
|
+
*
|
|
1364
|
+
* That zero is also why `detFromSchema` was off by one. Its formula replaces the
|
|
1365
|
+
* opaque placeholder with the schema's fields, and there was no placeholder to
|
|
1366
|
+
* replace, so the subtraction ate a real field instead.
|
|
1367
|
+
*/
|
|
1368
|
+
/**
|
|
1369
|
+
* Is this literal the argument of a call named `name`?
|
|
1370
|
+
*
|
|
1371
|
+
* Structural, because the test used to be a regex over the property's source text
|
|
1372
|
+
* (`/vine\.object/`) and Prettier breaks a long chain across lines:
|
|
1373
|
+
*
|
|
1374
|
+
* data: vine
|
|
1375
|
+
* .object({})
|
|
1376
|
+
* .allowUnknownProperties()
|
|
1377
|
+
*
|
|
1378
|
+
* `vine` and `.object` then sit on different lines, the regex misses, and the field
|
|
1379
|
+
* silently stops being recognised as an open object. A count that depends on where
|
|
1380
|
+
* the formatter put a newline is not a measurement — the same reason the
|
|
1381
|
+
* implementation-scope hash strips whitespace before hashing.
|
|
1382
|
+
*/
|
|
1383
|
+
function isArgumentOfCallNamed(literal, name) {
|
|
1384
|
+
const call = literal.getParent();
|
|
1385
|
+
if (!call || !Node.isCallExpression(call)) return false;
|
|
1386
|
+
const callee = call.getExpression();
|
|
1387
|
+
return Node.isPropertyAccessExpression(callee) && callee.getName() === name;
|
|
1388
|
+
}
|
|
1389
|
+
/** The last member of a call's callee: `vine.group.if(…)` is `if`, however it is wrapped. */
|
|
1390
|
+
function calleeName(call) {
|
|
1391
|
+
const callee = call.getExpression();
|
|
1392
|
+
return Node.isPropertyAccessExpression(callee) ? callee.getName() : void 0;
|
|
1393
|
+
}
|
|
1394
|
+
/** …and the member before it, so `group.if` can be told from any other `if`. */
|
|
1395
|
+
function calleeOwner(call) {
|
|
1396
|
+
const callee = call.getExpression();
|
|
1397
|
+
if (!Node.isPropertyAccessExpression(callee)) return void 0;
|
|
1398
|
+
const owner = callee.getExpression();
|
|
1399
|
+
return Node.isPropertyAccessExpression(owner) ? owner.getName() : void 0;
|
|
1400
|
+
}
|
|
1401
|
+
function leavesOf(node, resolveRef) {
|
|
1348
1402
|
const object = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1349
1403
|
if (!object) return {
|
|
1350
1404
|
leaves: [],
|
|
@@ -1356,9 +1410,8 @@ function leavesOf(node) {
|
|
|
1356
1410
|
for (const property of literal.getProperties()) {
|
|
1357
1411
|
if (!Node.isPropertyAssignment(property)) continue;
|
|
1358
1412
|
const name = property.getName().replace(/['"]/g, "");
|
|
1359
|
-
const text = property.getText();
|
|
1360
1413
|
const nested = property.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1361
|
-
if (nested &&
|
|
1414
|
+
if (nested && isArgumentOfCallNamed(nested, "object")) {
|
|
1362
1415
|
const path = prefix ? `${prefix}.${name}` : name;
|
|
1363
1416
|
/**
|
|
1364
1417
|
* An object declaring no properties enumerates nothing. It is the field
|
|
@@ -1378,6 +1431,20 @@ function leavesOf(node) {
|
|
|
1378
1431
|
};
|
|
1379
1432
|
walk(object, "");
|
|
1380
1433
|
/**
|
|
1434
|
+
* Conditional groups: `vine.object({}).merge(vine.group([vine.group.if(p, {…})]))`.
|
|
1435
|
+
*
|
|
1436
|
+
* The branches are mutually exclusive at runtime and the transaction can carry
|
|
1437
|
+
* any of them, so §7.2 counts the UNION — the fields the elementary process
|
|
1438
|
+
* handles. Read from the first object literal alone, the whole validator looked
|
|
1439
|
+
* like an open object and the count said the fields were data when they are
|
|
1440
|
+
* plainly in the code: five fields reported as one, and `detFromSchema` could not
|
|
1441
|
+
* fix it, because a group is not a JSON Schema.
|
|
1442
|
+
*
|
|
1443
|
+
* A `group.if` literal inside the outer object would be a nested schema `walk`
|
|
1444
|
+
* already handled, so only the ones outside it are roots here.
|
|
1445
|
+
*/
|
|
1446
|
+
for (const branch of groupBranchesIn(node, object, resolveRef)) walk(branch, "");
|
|
1447
|
+
/**
|
|
1381
1448
|
* The whole validator is an open object: nothing is enumerable, and the body
|
|
1382
1449
|
* that carries it is measured at the floor. Counted as one, reported as such.
|
|
1383
1450
|
*/
|
|
@@ -1386,10 +1453,38 @@ function leavesOf(node) {
|
|
|
1386
1453
|
opaque: ["*"]
|
|
1387
1454
|
};
|
|
1388
1455
|
return {
|
|
1389
|
-
leaves,
|
|
1390
|
-
opaque
|
|
1456
|
+
leaves: [...new Set(leaves)],
|
|
1457
|
+
opaque: [...new Set(opaque)]
|
|
1391
1458
|
};
|
|
1392
1459
|
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Object literals passed to `vine.group.if(…)`, following `.merge(x)` when `x` is
|
|
1462
|
+
* a name this file can resolve.
|
|
1463
|
+
*
|
|
1464
|
+
* The groups usually live in their own constant — which is why following the
|
|
1465
|
+
* reference matters more than recognising the inline form.
|
|
1466
|
+
*/
|
|
1467
|
+
function groupBranchesIn(node, outer, resolveRef, depth = 0) {
|
|
1468
|
+
if (depth > 3) return [];
|
|
1469
|
+
const found = [];
|
|
1470
|
+
for (const call of node.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1471
|
+
const method = calleeName(call);
|
|
1472
|
+
if ((method === "if" || method === "else") && calleeOwner(call) === "group") {
|
|
1473
|
+
for (const argument of call.getArguments()) {
|
|
1474
|
+
const literal = argument.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1475
|
+
if (literal && !outer.getDescendants().includes(literal)) found.push(literal);
|
|
1476
|
+
}
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
/** `.merge(openaiOrAws)`: the group is declared elsewhere */
|
|
1480
|
+
if (method !== "merge" || !resolveRef) continue;
|
|
1481
|
+
const reference = call.getArguments()[0];
|
|
1482
|
+
if (!reference || !Node.isIdentifier(reference)) continue;
|
|
1483
|
+
const declaration = resolveRef(reference.getText());
|
|
1484
|
+
if (declaration) found.push(...groupBranchesIn(declaration, outer, resolveRef, depth + 1));
|
|
1485
|
+
}
|
|
1486
|
+
return found;
|
|
1487
|
+
}
|
|
1393
1488
|
/** file name, to identify the unresolved call without dumping the full path */
|
|
1394
1489
|
const pathOf = (file) => file.split("/").pop()?.replace(/\.ts$/, "") ?? file;
|
|
1395
1490
|
const DEFAULT_MAX_DEPTH = 3;
|
|
@@ -2104,7 +2199,10 @@ function countTransactionalFunctions(entryPoints, behaviors, options) {
|
|
|
2104
2199
|
rule: behavior.writes ? "afp:6.5.3 modifies a data store -> EI" : "afp:6.5.3 uses without modifying -> EO (EQ collapsed per 6.5.3)",
|
|
2105
2200
|
detSources: sources,
|
|
2106
2201
|
refSources: touched.map((store) => `reaches:${store}`),
|
|
2107
|
-
trace: behavior.trace
|
|
2202
|
+
trace: behavior.trace.map((step) => ({
|
|
2203
|
+
...step,
|
|
2204
|
+
file: relativeTo(options.root, step.file)
|
|
2205
|
+
}))
|
|
2108
2206
|
}
|
|
2109
2207
|
});
|
|
2110
2208
|
}
|
|
@@ -2250,7 +2348,7 @@ const RULESET = "afp";
|
|
|
2250
2348
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
2251
2349
|
* exists for exactly that, and only this constant arms it.
|
|
2252
2350
|
*/
|
|
2253
|
-
const RULESET_VERSION = "1.
|
|
2351
|
+
const RULESET_VERSION = "1.3.0";
|
|
2254
2352
|
function count(input, options = {}) {
|
|
2255
2353
|
const warnings = [];
|
|
2256
2354
|
const usage = usageOf(input);
|
|
@@ -2296,14 +2394,38 @@ function count(input, options = {}) {
|
|
|
2296
2394
|
const ignored = new Set(options.boundary?.ignoreEntryPoints ?? []);
|
|
2297
2395
|
const transactionalFunctions = countTransactionalFunctions(input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? "")), input.behaviors, {
|
|
2298
2396
|
countedStores,
|
|
2397
|
+
root: input.app.root,
|
|
2299
2398
|
messageDet: options.messageDet ?? 0,
|
|
2300
2399
|
tables,
|
|
2301
2400
|
weights
|
|
2302
2401
|
});
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2402
|
+
/**
|
|
2403
|
+
* `opaqueReviewed` answers a warning that is CORRECT and therefore permanent.
|
|
2404
|
+
*
|
|
2405
|
+
* 1 DET for an opaque column is a floor, and `fp:count` says so on every run. But
|
|
2406
|
+
* some of those columns are one field — a copy, a checksum, a bag of metadata —
|
|
2407
|
+
* and there was no way to record that someone had looked. A warning that cannot be
|
|
2408
|
+
* answered is one the team learns to scroll past, which costs more than the warning
|
|
2409
|
+
* reports. It silences nothing else: the count does not move, and how many were
|
|
2410
|
+
* reviewed is still printed.
|
|
2411
|
+
*/
|
|
2412
|
+
const reviewed = reviewedOpaque(options.overrides ?? {});
|
|
2306
2413
|
const functions = applyOverrides([...dataFunctions, ...transactionalFunctions], options.overrides ?? {}, input.jsonSchemas ?? /* @__PURE__ */ new Map(), tables, weights, warnings);
|
|
2414
|
+
/**
|
|
2415
|
+
* Reported AFTER the overrides are applied, because the overrides are the answer
|
|
2416
|
+
* to it.
|
|
2417
|
+
*
|
|
2418
|
+
* Computed first, the list kept naming functions whose floor had already been
|
|
2419
|
+
* replaced by `detFromSchema` — telling the reader to go and map something that
|
|
2420
|
+
* was mapped. It cost a real misreading: a report was taken as "two forms still
|
|
2421
|
+
* unmapped" when both were declared, by whoever wrote this code.
|
|
2422
|
+
*/
|
|
2423
|
+
const declared = new Set(functions.filter((fn) => fn.rationale.overrides?.some((o) => o.fields.includes("det"))).map((fn) => fn.name));
|
|
2424
|
+
warnings.push(...opaqueWarnings(countable, input, {
|
|
2425
|
+
reviewed,
|
|
2426
|
+
declared
|
|
2427
|
+
}));
|
|
2428
|
+
warnings.push(...unreadableInputWarnings(input));
|
|
2307
2429
|
return {
|
|
2308
2430
|
ruleset: "afp",
|
|
2309
2431
|
rulesetVersion: RULESET_VERSION,
|
|
@@ -2325,45 +2447,93 @@ function count(input, options = {}) {
|
|
|
2325
2447
|
* `metadata` column changes no number, and warning about it would be the noise
|
|
2326
2448
|
* that teaches people to stop reading the confidence block.
|
|
2327
2449
|
*/
|
|
2328
|
-
|
|
2450
|
+
/**
|
|
2451
|
+
* Opaque DETs someone has declared reviewed, in both spellings a person might use.
|
|
2452
|
+
*
|
|
2453
|
+
* Qualified (`Petition.schema`) is unambiguous; bare (`schema`) is what someone reads
|
|
2454
|
+
* off the warning line.
|
|
2455
|
+
*/
|
|
2456
|
+
function reviewedOpaque(overrides) {
|
|
2457
|
+
const reviewed = /* @__PURE__ */ new Set();
|
|
2458
|
+
for (const [name, override] of Object.entries(overrides)) for (const entry of override.opaqueReviewed ?? []) {
|
|
2459
|
+
reviewed.add(entry);
|
|
2460
|
+
reviewed.add(`${name}.${entry}`);
|
|
2461
|
+
}
|
|
2462
|
+
return reviewed;
|
|
2463
|
+
}
|
|
2464
|
+
/** a column whose shape says nothing about what it holds */
|
|
2465
|
+
const OPAQUE_TYPE = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
2466
|
+
/**
|
|
2467
|
+
* DETs the analysis cannot read: an opaque column, or an open input object.
|
|
2468
|
+
*
|
|
2469
|
+
* Both count 1, which is a FLOOR rather than a measurement, and counting-decisions §8
|
|
2470
|
+
* is the trade. Reporting it is the point — this was the one known blind spot the
|
|
2471
|
+
* package reported nowhere.
|
|
2472
|
+
*
|
|
2473
|
+
* Grouped by FUNCTION and stating what has already been answered, because a flat list
|
|
2474
|
+
* of columns could not say that. Computed before the overrides ran, it named functions
|
|
2475
|
+
* whose floor `detFromSchema` had already replaced, which reads as "go and map this"
|
|
2476
|
+
* about something already mapped. That misreading actually happened, to the author of
|
|
2477
|
+
* this code, reading someone else's report.
|
|
2478
|
+
*
|
|
2479
|
+
* Three states per function, and only the third is a request to do something:
|
|
2480
|
+
*
|
|
2481
|
+
* replaced a `detFromSchema` override stands in for one of them
|
|
2482
|
+
* reviewed someone looked and 1 is the right answer
|
|
2483
|
+
* floor still unanswered
|
|
2484
|
+
*/
|
|
2485
|
+
function opaqueWarnings(stores, input, state) {
|
|
2329
2486
|
const reached = /* @__PURE__ */ new Map();
|
|
2330
2487
|
for (const entry of input.entryPoints) for (const store of input.behaviors.get(entry.id)?.touches ?? []) reached.set(store, (reached.get(store) ?? 0) + 1);
|
|
2331
|
-
const
|
|
2488
|
+
const byFunction = /* @__PURE__ */ new Map();
|
|
2489
|
+
const tally = (name, kind, transactions) => {
|
|
2490
|
+
const found = byFunction.get(name) ?? {
|
|
2491
|
+
kind,
|
|
2492
|
+
floor: [],
|
|
2493
|
+
reviewed: 0,
|
|
2494
|
+
transactions
|
|
2495
|
+
};
|
|
2496
|
+
byFunction.set(name, found);
|
|
2497
|
+
return found;
|
|
2498
|
+
};
|
|
2332
2499
|
for (const store of stores) {
|
|
2333
|
-
|
|
2334
|
-
if (!transactions) continue;
|
|
2500
|
+
if (!reached.get(store.name)) continue;
|
|
2335
2501
|
for (const attribute of store.attributes) {
|
|
2336
2502
|
if (!attribute.type || !OPAQUE_TYPE.test(attribute.type)) continue;
|
|
2337
|
-
|
|
2503
|
+
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)) entry.reviewed += 1;
|
|
2505
|
+
else entry.floor.push(`${attribute.name} (${attribute.type})`);
|
|
2338
2506
|
}
|
|
2339
2507
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
* Input fields declared as an open object: `vine.object({}).allowUnknownProperties()`.
|
|
2345
|
-
*
|
|
2346
|
-
* The same blind spot as an opaque column and, until now, reported nowhere — the
|
|
2347
|
-
* opaque-column warning names stores, and this one is on the transaction side, so
|
|
2348
|
-
* a route whose whole form arrives through one of these was invisible in the
|
|
2349
|
-
* confidence block. On a production application that was the route that saves the
|
|
2350
|
-
* main document, and it was the reason its EI never looked wrong.
|
|
2351
|
-
*
|
|
2352
|
-
* It counts 1 DET, which is a floor. When the fields are declared in the code
|
|
2353
|
-
* somewhere — a seeder, a schema module — `detFromSchema` replaces the floor with
|
|
2354
|
-
* the real count, and §8 of counting-decisions says how.
|
|
2355
|
-
*/
|
|
2356
|
-
function openValidatorWarnings(input) {
|
|
2357
|
-
const found = [];
|
|
2358
|
-
for (const entry of input.entryPoints) {
|
|
2359
|
-
const fields = input.behaviors.get(entry.id)?.opaqueInputFields ?? [];
|
|
2360
|
-
for (const field of fields) found.push(` ${entry.trigger} ${entry.signature} — ${field}`);
|
|
2508
|
+
for (const point of input.entryPoints) for (const field of input.behaviors.get(point.id)?.opaqueInputFields ?? []) {
|
|
2509
|
+
const entry = tally(point.identity, "input object", 1);
|
|
2510
|
+
if (state.reviewed.has(field) || state.reviewed.has(`${point.identity}.${field}`)) entry.reviewed += 1;
|
|
2511
|
+
else entry.floor.push(field);
|
|
2361
2512
|
}
|
|
2362
|
-
|
|
2513
|
+
const lines = [];
|
|
2514
|
+
let answered = 0;
|
|
2515
|
+
for (const [name, entry] of byFunction) {
|
|
2516
|
+
/** a declared schema stands in for exactly one placeholder — §8, and the override warns when there are more */
|
|
2517
|
+
const replaced = state.declared.has(name) && entry.floor.length > 0 ? 1 : 0;
|
|
2518
|
+
const remaining = entry.floor.slice(replaced);
|
|
2519
|
+
if (remaining.length === 0) {
|
|
2520
|
+
answered += 1;
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
const answeredHere = [...replaced > 0 ? [`${replaced} replaced by override`] : [], ...entry.reviewed > 0 ? [`${entry.reviewed} reviewed`] : []];
|
|
2524
|
+
/**
|
|
2525
|
+
* How many transactions reach the store, so the reader can judge whether the
|
|
2526
|
+
* floor is worth answering. A blob nothing touches changes no number.
|
|
2527
|
+
*/
|
|
2528
|
+
const reach = entry.kind === "column" ? `, reached by ${entry.transactions} transaction(s)` : "";
|
|
2529
|
+
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
|
+
}
|
|
2531
|
+
const settled = answered === 0 ? [] : [` (${answered} more function(s) whose opaque DETs are all accounted for)`];
|
|
2532
|
+
if (lines.length === 0) return settled;
|
|
2363
2533
|
return [
|
|
2364
|
-
`${
|
|
2365
|
-
...
|
|
2366
|
-
...
|
|
2534
|
+
`${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
|
+
...lines,
|
|
2536
|
+
...settled
|
|
2367
2537
|
];
|
|
2368
2538
|
}
|
|
2369
2539
|
/**
|
|
@@ -2402,8 +2572,6 @@ function unreadableInputWarnings(input) {
|
|
|
2402
2572
|
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
2403
2573
|
];
|
|
2404
2574
|
}
|
|
2405
|
-
/** a column whose shape says nothing about what it holds */
|
|
2406
|
-
const OPAQUE_TYPE = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
2407
2575
|
/**
|
|
2408
2576
|
* Replaces what the analysis found with what a person declared.
|
|
2409
2577
|
*
|
|
@@ -2438,7 +2606,25 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2438
2606
|
* is born, not when a field is.
|
|
2439
2607
|
*/
|
|
2440
2608
|
if (override.detFromSchema) {
|
|
2441
|
-
|
|
2609
|
+
/**
|
|
2610
|
+
* One name or several, unioned by leaf path.
|
|
2611
|
+
*
|
|
2612
|
+
* An ILF's DETs are the fields the user recognises in the file, and an
|
|
2613
|
+
* application with one schema per template recognises all of them. A field
|
|
2614
|
+
* two templates share is one DET, so the union is over paths rather than a
|
|
2615
|
+
* sum of counts.
|
|
2616
|
+
*/
|
|
2617
|
+
const named = [override.detFromSchema].flat();
|
|
2618
|
+
const resolved = named.map((name) => schemas.get(name)).filter((s) => s !== void 0);
|
|
2619
|
+
const missing = named.filter((name) => !schemas.has(name));
|
|
2620
|
+
const union = new Set(resolved.flatMap((s) => s.leaves));
|
|
2621
|
+
const schema = resolved.length === 0 ? void 0 : {
|
|
2622
|
+
/** only what resolved: naming a schema that contributed nothing would mislead */
|
|
2623
|
+
name: resolved.map((s) => s.name).join(" + "),
|
|
2624
|
+
fields: union.size,
|
|
2625
|
+
leaves: [...union]
|
|
2626
|
+
};
|
|
2627
|
+
for (const name of missing) warnings.push(`override for "${fn.name}" names schema "${name}", which is not declared anywhere in the code: it contributed nothing. A renamed or moved schema breaks the mapping, and this says so rather than counting on silently.`);
|
|
2442
2628
|
if (schema) {
|
|
2443
2629
|
/**
|
|
2444
2630
|
* Replaces the opaque placeholder — the one the rationale marks — rather
|
|
@@ -2454,10 +2640,20 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2454
2640
|
by = `config:overrides.${fn.name} (from ${schema.name}: ${schema.fields} fields)`;
|
|
2455
2641
|
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.`);
|
|
2456
2642
|
else if (placeholders.length > 1) warnings.push(`override for "${fn.name}" names one schema and the function has ${placeholders.length} opaque DETs (${placeholders.join(", ")}). Only one was replaced; the others still count 1 each.`);
|
|
2457
|
-
}
|
|
2643
|
+
}
|
|
2458
2644
|
}
|
|
2459
2645
|
const refs = override.refs ?? fn.refs;
|
|
2460
2646
|
const complexity = complexityOf(fn.type, refs, det, tables);
|
|
2647
|
+
/**
|
|
2648
|
+
* Only a DECLARED NUMBER is an override in the rationale.
|
|
2649
|
+
*
|
|
2650
|
+
* `fp:count` prints what share of the total came from a person, and that line is
|
|
2651
|
+
* the reason the mechanism is acceptable at all. An `opaqueReviewed`-only entry
|
|
2652
|
+
* declares no number, and recording it here read as "1 function, 7 FP, 35% of the
|
|
2653
|
+
* total declared by override" — misrepresenting the one number that exists to keep
|
|
2654
|
+
* this honest. The review is recorded in the warning, which is where it belongs.
|
|
2655
|
+
*/
|
|
2656
|
+
if (fields.length === 0) return fn;
|
|
2461
2657
|
return {
|
|
2462
2658
|
...fn,
|
|
2463
2659
|
det,
|
|
@@ -2601,7 +2797,7 @@ function describeSource(root, config) {
|
|
|
2601
2797
|
]),
|
|
2602
2798
|
dirty: status === void 0 ? void 0 : status.length > 0,
|
|
2603
2799
|
countedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2604
|
-
config: config === null ? null :
|
|
2800
|
+
config: config === null ? null : relativeTo(root, config)
|
|
2605
2801
|
};
|
|
2606
2802
|
}
|
|
2607
2803
|
function appName(root) {
|
|
@@ -2635,17 +2831,51 @@ async function analyze(root, options = {}) {
|
|
|
2635
2831
|
const behaviors = new Map(entryPoints.filter((entry) => entry.handler).map((entry) => [entry.id, analyzer.analyze(entry.handler)]));
|
|
2636
2832
|
const resolved = [...behaviors.values()].filter((behavior) => behavior.unresolved.length === 0).length;
|
|
2637
2833
|
const unresolvedCalls = storeProblems.length + routeProblems.length + [...behaviors.values()].reduce((total, behavior) => total + behavior.unresolved.length, 0);
|
|
2834
|
+
/**
|
|
2835
|
+
* Every path that LEAVES is relative to the application root.
|
|
2836
|
+
*
|
|
2837
|
+
* `CountSource.app` is documented as never being the absolute path, because that
|
|
2838
|
+
* says where the machine keeps its files and travels with every artefact sent
|
|
2839
|
+
* anywhere. The rule was stated on one field and applied to one field: the
|
|
2840
|
+
* inventory carried 2036 absolute paths across ten of them, and a count carried
|
|
2841
|
+
* 858 in its traces alone.
|
|
2842
|
+
*
|
|
2843
|
+
* Absolute is right INTERNALLY — it is what ts-morph resolves and what the call
|
|
2844
|
+
* graph keys its caches on — so the conversion happens here, at the boundary, and
|
|
2845
|
+
* the data-store `id` is relativised only after the ancestor filter has used it.
|
|
2846
|
+
*/
|
|
2847
|
+
const source = describeSource(root, options.configFile ?? null);
|
|
2848
|
+
const emit = (value) => relativeTo(app.root, value);
|
|
2849
|
+
const emitProvenance = (p) => ({
|
|
2850
|
+
...p,
|
|
2851
|
+
file: emit(p.file)
|
|
2852
|
+
});
|
|
2638
2853
|
const inventory = {
|
|
2639
2854
|
version: 1,
|
|
2640
2855
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2641
|
-
app: app
|
|
2856
|
+
app: source.app,
|
|
2642
2857
|
framework: {
|
|
2643
2858
|
core: app.framework.core,
|
|
2644
2859
|
lucid: app.framework.lucid,
|
|
2645
2860
|
orm: app.framework.orm
|
|
2646
2861
|
},
|
|
2647
|
-
dataStores: stores
|
|
2648
|
-
|
|
2862
|
+
dataStores: stores.map((store) => ({
|
|
2863
|
+
...store,
|
|
2864
|
+
id: emit(store.id),
|
|
2865
|
+
provenance: emitProvenance(store.provenance),
|
|
2866
|
+
attributes: store.attributes.map((a) => ({
|
|
2867
|
+
...a,
|
|
2868
|
+
provenance: emitProvenance(a.provenance)
|
|
2869
|
+
}))
|
|
2870
|
+
})),
|
|
2871
|
+
entryPoints: entryPoints.map((entry) => ({
|
|
2872
|
+
...entry,
|
|
2873
|
+
provenance: emitProvenance(entry.provenance),
|
|
2874
|
+
handler: entry.handler ? {
|
|
2875
|
+
...entry.handler,
|
|
2876
|
+
file: emit(entry.handler.file)
|
|
2877
|
+
} : null
|
|
2878
|
+
})),
|
|
2649
2879
|
behaviors: [...behaviors.entries()].map(([entryPointId, behavior]) => ({
|
|
2650
2880
|
entryPointId,
|
|
2651
2881
|
writes: behavior.writes,
|
|
@@ -2653,28 +2883,34 @@ async function analyze(root, options = {}) {
|
|
|
2653
2883
|
inputFields: behavior.inputFields.map((name) => ({
|
|
2654
2884
|
name,
|
|
2655
2885
|
provenance: {
|
|
2656
|
-
file: app.root,
|
|
2886
|
+
file: emit(app.root),
|
|
2657
2887
|
by: "validator"
|
|
2658
2888
|
}
|
|
2659
2889
|
})),
|
|
2660
2890
|
opaqueInputFields: behavior.opaqueInputFields.map((name) => ({
|
|
2661
2891
|
name,
|
|
2662
2892
|
provenance: {
|
|
2663
|
-
file: app.root,
|
|
2893
|
+
file: emit(app.root),
|
|
2664
2894
|
by: "validator"
|
|
2665
2895
|
}
|
|
2666
2896
|
})),
|
|
2667
2897
|
requestFields: behavior.requestFields.map((name) => ({
|
|
2668
2898
|
name,
|
|
2669
2899
|
provenance: {
|
|
2670
|
-
file: app.root,
|
|
2900
|
+
file: emit(app.root),
|
|
2671
2901
|
by: "request"
|
|
2672
2902
|
}
|
|
2673
2903
|
})),
|
|
2674
2904
|
opaqueRequest: behavior.opaqueRequest,
|
|
2675
2905
|
outputFields: [],
|
|
2676
|
-
trace: behavior.trace
|
|
2677
|
-
|
|
2906
|
+
trace: behavior.trace.map((step) => ({
|
|
2907
|
+
...step,
|
|
2908
|
+
file: emit(step.file)
|
|
2909
|
+
})),
|
|
2910
|
+
unresolved: behavior.unresolved.map((call) => ({
|
|
2911
|
+
...call,
|
|
2912
|
+
file: emit(call.file)
|
|
2913
|
+
}))
|
|
2678
2914
|
})),
|
|
2679
2915
|
coverage: {
|
|
2680
2916
|
entryPointsTotal: entryPoints.length,
|
|
@@ -2696,7 +2932,7 @@ async function analyze(root, options = {}) {
|
|
|
2696
2932
|
jsonSchemas,
|
|
2697
2933
|
writtenAnywhere: analyzer.writtenAnywhere()
|
|
2698
2934
|
}, options),
|
|
2699
|
-
source
|
|
2935
|
+
source
|
|
2700
2936
|
}
|
|
2701
2937
|
};
|
|
2702
2938
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
2
3
|
//#region src/inventory/paths.ts
|
|
3
4
|
/**
|
|
@@ -20,6 +21,23 @@ import { Node, Project, SyntaxKind } from "ts-morph";
|
|
|
20
21
|
* it to each comparison costs vigilance forever.
|
|
21
22
|
*/
|
|
22
23
|
const toPosix = (value) => value.split("\\").join("/");
|
|
24
|
+
/**
|
|
25
|
+
* A path as it should appear in an EMITTED artefact: relative to the application.
|
|
26
|
+
*
|
|
27
|
+
* `CountSource.app` is documented as never being the absolute path, because that
|
|
28
|
+
* says where the machine keeps its files and travels with every count sent
|
|
29
|
+
* anywhere. One field below it, `config` shipped the absolute path — and so did
|
|
30
|
+
* every `trace[].file`, 858 times in a single production count. The rule was
|
|
31
|
+
* stated and then applied to one field.
|
|
32
|
+
*
|
|
33
|
+
* Internally the absolute path is the right thing: it is what ts-morph resolves
|
|
34
|
+
* and what the call graph keys its caches on. So this converts at the boundary
|
|
35
|
+
* where a path LEAVES, and nowhere else.
|
|
36
|
+
*
|
|
37
|
+
* A path outside the root keeps its `../` prefix, which describes where it is
|
|
38
|
+
* without naming the home directory.
|
|
39
|
+
*/
|
|
40
|
+
const relativeTo = (root, value) => toPosix(path.relative(toPosix(root), toPosix(value))) || ".";
|
|
23
41
|
/** Compares two paths that may have come from different sources. */
|
|
24
42
|
const samePath = (a, b) => a !== void 0 && b !== void 0 && toPosix(a) === toPosix(b);
|
|
25
43
|
//#endregion
|
|
@@ -819,4 +837,4 @@ function resolveCall(call, ctx, resolvers = BUILTIN_CALL_RESOLVERS) {
|
|
|
819
837
|
return null;
|
|
820
838
|
}
|
|
821
839
|
//#endregion
|
|
822
|
-
export { rootSymbolOf as a,
|
|
840
|
+
export { rootSymbolOf as a, samePath as c, hooksFiredBy as i, toPosix as l, resolveCall as n, collectEventBindings as o, detectAccess as r, relativeTo as s, BUILTIN_CALL_RESOLVERS as t };
|
|
@@ -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 { l as toPosix } from "./resolvers-vMahHkAd.js";
|
|
3
|
+
import { n as analyze } from "./pipeline-Dm9KvUvF.js";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -359,7 +359,17 @@ async function runMetrics(options) {
|
|
|
359
359
|
async function runExplain(options) {
|
|
360
360
|
const { config, notes } = await configFor(options.root);
|
|
361
361
|
const { count } = await analyze(options.root, config);
|
|
362
|
-
|
|
362
|
+
/**
|
|
363
|
+
* An exact name wins outright; the substring search is the fallback.
|
|
364
|
+
*
|
|
365
|
+
* `fp:explain "POST /orders/:param/submit"` returned four functions, because
|
|
366
|
+
* `/submit`, `/submit-ready` and `/submit-ready/return` all contain it. Asking
|
|
367
|
+
* about a function by its exact name and being handed its neighbours makes the
|
|
368
|
+
* command useless for the thing it exists for — defending one number.
|
|
369
|
+
*/
|
|
370
|
+
const wanted = options.name.toLowerCase();
|
|
371
|
+
const exact = count.functions.filter((fn) => fn.name.toLowerCase() === wanted);
|
|
372
|
+
const matched = exact.length > 0 ? exact : count.functions.filter((fn) => fn.name.toLowerCase().includes(wanted));
|
|
363
373
|
if (matched.length === 0) return {
|
|
364
374
|
output: "",
|
|
365
375
|
notes,
|
|
@@ -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.3.0";
|
|
38
38
|
export type CountInput = {
|
|
39
39
|
app: AppContext;
|
|
40
40
|
stores: CollectedDataStore[];
|
|
@@ -31,5 +31,14 @@ export type TransactionOptions = {
|
|
|
31
31
|
messageDet: number;
|
|
32
32
|
tables: Record<FunctionType, ComplexityTable>;
|
|
33
33
|
weights: Record<FunctionType, Record<Complexity, number>>;
|
|
34
|
+
/**
|
|
35
|
+
* Application root, used only to relativise the paths that LEAVE in the trace.
|
|
36
|
+
*
|
|
37
|
+
* `CountSource.app` is documented as never being the absolute path, because it
|
|
38
|
+
* says where the machine keeps its files and travels with every count sent
|
|
39
|
+
* anywhere. The trace shipped the absolute path regardless — 858 times in a
|
|
40
|
+
* single production count, which is most of the artefact a ledger would store.
|
|
41
|
+
*/
|
|
42
|
+
root: string;
|
|
34
43
|
};
|
|
35
44
|
export declare function countTransactionalFunctions(entryPoints: CollectedEntryPoint[], behaviors: Map<string, Behavior>, options: TransactionOptions): CountedFunction[];
|
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-Dm9KvUvF.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-DetZGfh5.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -161,9 +161,34 @@ export type FunctionOverride = {
|
|
|
161
161
|
*
|
|
162
162
|
* A name that matches no schema is a warning, never a silent fallback.
|
|
163
163
|
*/
|
|
164
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Name of a declared schema, or several whose fields are UNIONED.
|
|
166
|
+
*
|
|
167
|
+
* An ILF's DETs are the fields the user recognises in the file, and an
|
|
168
|
+
* application with one schema per template recognises the fields of all of them.
|
|
169
|
+
* Pointing at the largest and justifying it in `reason` gives the same answer
|
|
170
|
+
* only while they land in the same complexity band — which is a piece of
|
|
171
|
+
* reasoning the configuration should not have to carry.
|
|
172
|
+
*
|
|
173
|
+
* Unioned by leaf path, so a field two templates share counts once.
|
|
174
|
+
*/
|
|
175
|
+
detFromSchema?: string | string[];
|
|
165
176
|
/** declared RET (data function) or FTR (transaction) */
|
|
166
177
|
refs?: number;
|
|
178
|
+
/**
|
|
179
|
+
* Opaque DETs someone has looked at and decided are correct at 1.
|
|
180
|
+
*
|
|
181
|
+
* `fp:count` reports every opaque column and open input object, because 1 DET is
|
|
182
|
+
* a floor rather than a measurement. But some of them ARE one field — a copy, a
|
|
183
|
+
* checksum, a bag of metadata — and there was no way to say so, so the warning
|
|
184
|
+
* fired on every run forever. A warning that cannot be answered is a warning the
|
|
185
|
+
* team learns to scroll past, which costs more than the one it reports.
|
|
186
|
+
*
|
|
187
|
+
* It silences nothing else: the count does not move, and `fp:count` still says
|
|
188
|
+
* how many were reviewed. Names are matched bare (`schema`) or qualified
|
|
189
|
+
* (`Petition.schema`).
|
|
190
|
+
*/
|
|
191
|
+
opaqueReviewed?: string[];
|
|
167
192
|
/** why — required, and printed by `fp:explain` beside the number */
|
|
168
193
|
reason: string;
|
|
169
194
|
};
|
|
@@ -18,5 +18,22 @@
|
|
|
18
18
|
* it to each comparison costs vigilance forever.
|
|
19
19
|
*/
|
|
20
20
|
export declare const toPosix: (value: string) => string;
|
|
21
|
+
/**
|
|
22
|
+
* A path as it should appear in an EMITTED artefact: relative to the application.
|
|
23
|
+
*
|
|
24
|
+
* `CountSource.app` is documented as never being the absolute path, because that
|
|
25
|
+
* says where the machine keeps its files and travels with every count sent
|
|
26
|
+
* anywhere. One field below it, `config` shipped the absolute path — and so did
|
|
27
|
+
* every `trace[].file`, 858 times in a single production count. The rule was
|
|
28
|
+
* stated and then applied to one field.
|
|
29
|
+
*
|
|
30
|
+
* Internally the absolute path is the right thing: it is what ts-morph resolves
|
|
31
|
+
* and what the call graph keys its caches on. So this converts at the boundary
|
|
32
|
+
* where a path LEAVES, and nowhere else.
|
|
33
|
+
*
|
|
34
|
+
* A path outside the root keeps its `../` prefix, which describes where it is
|
|
35
|
+
* without naming the home directory.
|
|
36
|
+
*/
|
|
37
|
+
export declare const relativeTo: (root: string, value: string) => string;
|
|
21
38
|
/** Compares two paths that may have come from different sources. */
|
|
22
39
|
export declare const samePath: (a: string | undefined, b: string | undefined) => boolean;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-
|
|
1
|
+
import { n as resolveCall, t as BUILTIN_CALL_RESOLVERS } from "../../../resolvers-vMahHkAd.js";
|
|
2
2
|
export { BUILTIN_CALL_RESOLVERS, resolveCall };
|
|
@@ -33,7 +33,14 @@ export type CountSource = {
|
|
|
33
33
|
*/
|
|
34
34
|
dirty?: boolean;
|
|
35
35
|
countedAt: string;
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Configuration file that shaped the count, RELATIVE to the application root,
|
|
38
|
+
* or null for the defaults.
|
|
39
|
+
*
|
|
40
|
+
* Relative for the same reason `app` is a name: an absolute path says where the
|
|
41
|
+
* machine keeps its files, and this artefact is what goes into a ledger and to
|
|
42
|
+
* whoever receives the invoice.
|
|
43
|
+
*/
|
|
37
44
|
config: string | null;
|
|
38
45
|
};
|
|
39
46
|
export declare function describeSource(root: string, config: string | null): CountSource;
|
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-Dm9KvUvF.js";
|
|
2
2
|
export { CoverageTooLowError, analyze };
|
package/build/stubs/config.stub
CHANGED
|
@@ -42,7 +42,13 @@ export default defineConfig({
|
|
|
42
42
|
* here.
|
|
43
43
|
*/
|
|
44
44
|
// overrides: {
|
|
45
|
-
//
|
|
45
|
+
// // one schema, or several whose fields are unioned by leaf path
|
|
46
|
+
// Form: { detFromSchema: ['intakeSchema', 'reviewSchema'], reason: 'one per template' },
|
|
47
|
+
//
|
|
48
|
+
// // 1 DET is a floor, and `fp:count` says so on every run. When 1 IS the right
|
|
49
|
+
// // answer, record that someone checked — otherwise the warning becomes noise
|
|
50
|
+
// // the team learns to scroll past. It moves no number.
|
|
51
|
+
// Petition: { opaqueReviewed: ['schema', 'uiSchema'], reason: 'metadata; one field each' },
|
|
46
52
|
// },
|
|
47
53
|
|
|
48
54
|
/**
|