@filipebraida/adonis-function-points 0.2.0 → 0.3.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 +45 -0
- package/README.md +1 -1
- package/build/calibration-8eV8CEix.js +403 -0
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-Cm079xWL.js → fp_calibrate-iFAec0tA.js} +1 -1
- package/build/{fp_count-CfcXPuj5.js → fp_count-D21tQ_pv.js} +1 -1
- package/build/{fp_diff-DE_t3twv.js → fp_diff-D0pHGMgi.js} +1 -1
- package/build/{fp_explain-MKyEoi0h.js → fp_explain-BwFs-LW-.js} +1 -1
- package/build/{fp_inventory-DSrCVhEy.js → fp_inventory-Bu6O1Nn0.js} +1 -1
- package/build/{fp_metrics-BUWj9dLw.js → fp_metrics-MGDppfSa.js} +1 -1
- package/build/index.d.ts +28 -0
- package/build/index.js +4 -3
- package/build/{pipeline-DySlMWcN.js → pipeline-CIAydCcT.js} +162 -22
- package/build/{resolvers-MFjRl2ef.js → resolvers-CRB6lXoo.js} +337 -290
- package/build/{runners-DpMd-yZM.js → runners-CmxNHuuq.js} +3 -387
- package/build/src/albrecht/counter.d.ts +9 -5
- package/build/src/cli.js +2 -2
- package/build/src/inventory/detectors/lucid.d.ts +8 -0
- package/build/src/inventory/graph/call_graph.d.ts +2 -0
- package/build/src/inventory/resolvers/index.js +1 -1
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +7 -0
- package/package.json +1 -1
- package/build/define_config-DOqWyPwV.js +0 -19
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as rootSymbolOf, c as toPosix, i as hooksFiredBy, n as resolveCall, o as collectEventBindings, r as detectAccess, s as samePath, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-
|
|
2
|
-
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
1
|
+
import { a as rootSymbolOf, c as toPosix, i as hooksFiredBy, n as resolveCall, o as collectEventBindings, r as detectAccess, s as samePath, t as BUILTIN_CALL_RESOLVERS } from "./resolvers-CRB6lXoo.js";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
@@ -956,7 +956,21 @@ function collectJsonSchemas(app) {
|
|
|
956
956
|
skipFileDependencyResolution: true,
|
|
957
957
|
compilerOptions: { allowJs: false }
|
|
958
958
|
});
|
|
959
|
-
|
|
959
|
+
/**
|
|
960
|
+
* Wider than `scanRoots`, and only here.
|
|
961
|
+
*
|
|
962
|
+
* `database/` is excluded from the application roots on purpose: test factories
|
|
963
|
+
* and migrations contain real persistence calls, and scanning them would turn a
|
|
964
|
+
* test write into a counted function. But `node ace make:seeder` puts seeders in
|
|
965
|
+
* `database/seeders`, which is where seed data — and therefore a form's schema —
|
|
966
|
+
* normally lives. Reading a literal counts nothing, so there is no conflict: the
|
|
967
|
+
* exclusion protects the call graph, not the schema catalogue.
|
|
968
|
+
*
|
|
969
|
+
* Without this, `overrides.detFromSchema` naming a schema declared in a seeder
|
|
970
|
+
* reported "not declared anywhere in the code" and left the DET count at the
|
|
971
|
+
* floor — the exact case the override exists for.
|
|
972
|
+
*/
|
|
973
|
+
for (const root of [...app.scanRoots.map(toPosix), `${toPosix(app.root)}/database`]) project.addSourceFilesAtPaths(`${root}/**/*.ts`);
|
|
960
974
|
const found = /* @__PURE__ */ new Map();
|
|
961
975
|
for (const file of project.getSourceFiles()) for (const declaration of file.getVariableDeclarations()) {
|
|
962
976
|
const literal = unwrap(declaration.getInitializer())?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
@@ -1224,6 +1238,7 @@ function isNoiseMember(file, member) {
|
|
|
1224
1238
|
*/
|
|
1225
1239
|
function validatorFieldsIn(body, file, app) {
|
|
1226
1240
|
const fields = [];
|
|
1241
|
+
const opaque = [];
|
|
1227
1242
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1228
1243
|
const expression = call.getExpression();
|
|
1229
1244
|
if (!Node.isPropertyAccessExpression(expression)) continue;
|
|
@@ -1233,9 +1248,18 @@ function validatorFieldsIn(body, file, app) {
|
|
|
1233
1248
|
const name = argument.getText();
|
|
1234
1249
|
const declaration = findValidator(name, file, app);
|
|
1235
1250
|
if (!declaration) continue;
|
|
1236
|
-
|
|
1251
|
+
const { leaves, opaque: unreadable } = leavesOf(declaration);
|
|
1252
|
+
const isOpaque = new Set(unreadable);
|
|
1253
|
+
for (const leaf of leaves) {
|
|
1254
|
+
const field = `${name}.${leaf}`;
|
|
1255
|
+
fields.push(field);
|
|
1256
|
+
if (isOpaque.has(leaf)) opaque.push(field);
|
|
1257
|
+
}
|
|
1237
1258
|
}
|
|
1238
|
-
return
|
|
1259
|
+
return {
|
|
1260
|
+
fields,
|
|
1261
|
+
opaque
|
|
1262
|
+
};
|
|
1239
1263
|
}
|
|
1240
1264
|
/**
|
|
1241
1265
|
* Fields read straight off the request, with no validator in between.
|
|
@@ -1320,11 +1344,14 @@ function findValidator(name, file, app) {
|
|
|
1320
1344
|
}
|
|
1321
1345
|
return null;
|
|
1322
1346
|
}
|
|
1323
|
-
/** leaves of a VineJS schema, per the table in §7 */
|
|
1324
1347
|
function leavesOf(node) {
|
|
1325
1348
|
const object = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1326
|
-
if (!object) return
|
|
1349
|
+
if (!object) return {
|
|
1350
|
+
leaves: [],
|
|
1351
|
+
opaque: []
|
|
1352
|
+
};
|
|
1327
1353
|
const leaves = [];
|
|
1354
|
+
const opaque = [];
|
|
1328
1355
|
const walk = (literal, prefix) => {
|
|
1329
1356
|
for (const property of literal.getProperties()) {
|
|
1330
1357
|
if (!Node.isPropertyAssignment(property)) continue;
|
|
@@ -1332,14 +1359,36 @@ function leavesOf(node) {
|
|
|
1332
1359
|
const text = property.getText();
|
|
1333
1360
|
const nested = property.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1334
1361
|
if (nested && /vine\.object/.test(text)) {
|
|
1335
|
-
|
|
1362
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
1363
|
+
/**
|
|
1364
|
+
* An object declaring no properties enumerates nothing. It is the field
|
|
1365
|
+
* itself that crosses the boundary, so it counts once — never zero,
|
|
1366
|
+
* which would make it cheaper than a plain string.
|
|
1367
|
+
*/
|
|
1368
|
+
if (nested.getProperties().length === 0) {
|
|
1369
|
+
leaves.push(path);
|
|
1370
|
+
opaque.push(path);
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
walk(nested, path);
|
|
1336
1374
|
continue;
|
|
1337
1375
|
}
|
|
1338
1376
|
leaves.push(prefix ? `${prefix}.${name}` : name);
|
|
1339
1377
|
}
|
|
1340
1378
|
};
|
|
1341
1379
|
walk(object, "");
|
|
1342
|
-
|
|
1380
|
+
/**
|
|
1381
|
+
* The whole validator is an open object: nothing is enumerable, and the body
|
|
1382
|
+
* that carries it is measured at the floor. Counted as one, reported as such.
|
|
1383
|
+
*/
|
|
1384
|
+
if (leaves.length === 0 && object.getProperties().length === 0) return {
|
|
1385
|
+
leaves: ["*"],
|
|
1386
|
+
opaque: ["*"]
|
|
1387
|
+
};
|
|
1388
|
+
return {
|
|
1389
|
+
leaves,
|
|
1390
|
+
opaque
|
|
1391
|
+
};
|
|
1343
1392
|
}
|
|
1344
1393
|
/** file name, to identify the unresolved call without dumping the full path */
|
|
1345
1394
|
const pathOf = (file) => file.split("/").pop()?.replace(/\.ts$/, "") ?? file;
|
|
@@ -1440,7 +1489,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1440
1489
|
const accesses = [];
|
|
1441
1490
|
const followUps = [];
|
|
1442
1491
|
const unresolved = [];
|
|
1443
|
-
const
|
|
1492
|
+
const validator = validatorFieldsIn(body, file, app);
|
|
1444
1493
|
const request = requestFieldsIn(body);
|
|
1445
1494
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1446
1495
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
@@ -1449,9 +1498,14 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1449
1498
|
store: access.store,
|
|
1450
1499
|
write: access.mode === "write"
|
|
1451
1500
|
});
|
|
1501
|
+
/**
|
|
1502
|
+
* A relation reached by `preload`/`load` is read; one written through
|
|
1503
|
+
* `related('files').create(…)` is written. Assuming read either way made
|
|
1504
|
+
* a table maintained only through a relation come out as an EIF.
|
|
1505
|
+
*/
|
|
1452
1506
|
if (access.viaRelation) accesses.push({
|
|
1453
1507
|
store: access.viaRelation,
|
|
1454
|
-
write:
|
|
1508
|
+
write: access.relationWritten === true
|
|
1455
1509
|
});
|
|
1456
1510
|
/**
|
|
1457
1511
|
* counting-decisions §3: a hook belongs to the transaction that fired
|
|
@@ -1499,7 +1553,8 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1499
1553
|
accesses,
|
|
1500
1554
|
followUps,
|
|
1501
1555
|
unresolved,
|
|
1502
|
-
validators,
|
|
1556
|
+
validators: validator.fields,
|
|
1557
|
+
opaqueValidators: validator.opaque,
|
|
1503
1558
|
requestFields: request.fields,
|
|
1504
1559
|
opaqueRequest: request.opaque,
|
|
1505
1560
|
bodyHash: hashOf(body)
|
|
@@ -1527,7 +1582,15 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1527
1582
|
if (symbols.size === 0) continue;
|
|
1528
1583
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1529
1584
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
1530
|
-
if (access?.mode
|
|
1585
|
+
if (access?.mode !== "write") continue;
|
|
1586
|
+
written.add(access.store);
|
|
1587
|
+
/**
|
|
1588
|
+
* `distribution.related('files').create(…)` maintains the related table.
|
|
1589
|
+
* Recording only the parent left a table written exclusively that way
|
|
1590
|
+
* looking like somebody else's — reported by a production application as
|
|
1591
|
+
* an EIF it was sure it maintained.
|
|
1592
|
+
*/
|
|
1593
|
+
if (access.viaRelation && access.relationWritten) written.add(access.viaRelation);
|
|
1531
1594
|
}
|
|
1532
1595
|
}
|
|
1533
1596
|
return written;
|
|
@@ -1541,6 +1604,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1541
1604
|
function run(handler) {
|
|
1542
1605
|
const touches = /* @__PURE__ */ new Set();
|
|
1543
1606
|
const inputFields = /* @__PURE__ */ new Set();
|
|
1607
|
+
const opaqueInputFields = /* @__PURE__ */ new Set();
|
|
1544
1608
|
const requestFields = /* @__PURE__ */ new Set();
|
|
1545
1609
|
let opaqueRequest = false;
|
|
1546
1610
|
const trace = [];
|
|
@@ -1580,6 +1644,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1580
1644
|
}
|
|
1581
1645
|
unresolved.push(...facts.unresolved);
|
|
1582
1646
|
for (const field of facts.validators) inputFields.add(field);
|
|
1647
|
+
for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
|
|
1583
1648
|
for (const field of facts.requestFields) requestFields.add(field);
|
|
1584
1649
|
if (facts.opaqueRequest) opaqueRequest = true;
|
|
1585
1650
|
trace.push({
|
|
@@ -1606,6 +1671,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1606
1671
|
writes,
|
|
1607
1672
|
touches: [...touches].sort(),
|
|
1608
1673
|
inputFields: [...inputFields].sort(),
|
|
1674
|
+
opaqueInputFields: [...opaqueInputFields].sort(),
|
|
1609
1675
|
requestFields: [...requestFields].sort(),
|
|
1610
1676
|
opaqueRequest,
|
|
1611
1677
|
trace,
|
|
@@ -1957,6 +2023,12 @@ function pointsOf(type, complexity, weights = DEFAULT_WEIGHTS) {
|
|
|
1957
2023
|
}
|
|
1958
2024
|
//#endregion
|
|
1959
2025
|
//#region src/albrecht/data_functions.ts
|
|
2026
|
+
/**
|
|
2027
|
+
* A column whose shape says nothing about what it holds. Marked in the rationale
|
|
2028
|
+
* because `detFromSchema` replaces exactly this placeholder, and because a reader
|
|
2029
|
+
* deserves to know which of the DETs is a floor rather than a count.
|
|
2030
|
+
*/
|
|
2031
|
+
const OPAQUE_TYPE$1 = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
1960
2032
|
function countDataFunctions(stores, usage, options) {
|
|
1961
2033
|
const counted = [];
|
|
1962
2034
|
for (const store of stores) {
|
|
@@ -1993,7 +2065,7 @@ function countDataFunctions(stores, usage, options) {
|
|
|
1993
2065
|
points: pointsOf(type, complexity, options.weights),
|
|
1994
2066
|
rationale: {
|
|
1995
2067
|
rule: options.externallyMaintained.has(store.name) ? "afp:6.5.4 externally maintained by boundary configuration -> EIF" : maintained ? "afp:6.5.4 maintained by an application transaction -> ILF" : "afp:6.5.4 used but not maintained -> EIF",
|
|
1996
|
-
detSources: detAttributes.map((attribute) => `${store.columnSource}:${store.table ?? store.name}.${attribute.name}`),
|
|
2068
|
+
detSources: detAttributes.map((attribute) => `${store.columnSource}:${store.table ?? store.name}.${attribute.name}` + (attribute.type && OPAQUE_TYPE$1.test(attribute.type) ? " (opaque)" : "")),
|
|
1997
2069
|
refSources: options.retStrategy === "composition" ? ["1 (main group)", ...store.subgroups.map((s) => `composition:${s}`)] : ["1 (constant: a logical subgroup is not derivable from code)"]
|
|
1998
2070
|
}
|
|
1999
2071
|
});
|
|
@@ -2077,7 +2149,14 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2077
2149
|
sources.push(source);
|
|
2078
2150
|
};
|
|
2079
2151
|
for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
|
|
2080
|
-
|
|
2152
|
+
/**
|
|
2153
|
+
* `(opaque)` is not decoration: `detFromSchema` replaces the opaque placeholder
|
|
2154
|
+
* with a schema's fields, and it used to assume there was exactly one and that
|
|
2155
|
+
* it was worth 1. An open `vine.object` counted zero, so the subtraction ate a
|
|
2156
|
+
* real field — off by one, in the direction of undercounting.
|
|
2157
|
+
*/
|
|
2158
|
+
const opaqueInputs = new Set(behavior.opaqueInputFields);
|
|
2159
|
+
for (const field of behavior.inputFields) add(field.split(".").pop(), `validator:${field}${opaqueInputs.has(field) ? " (opaque)" : ""}`);
|
|
2081
2160
|
/**
|
|
2082
2161
|
* §7.2 asks whether a user-recognisable field crosses the boundary, not how it
|
|
2083
2162
|
* was declared. `request.input('title')` does, and counted for nothing while
|
|
@@ -2141,6 +2220,17 @@ function isTechnical(store, patterns = DEFAULT_TECHNICAL_PATTERNS) {
|
|
|
2141
2220
|
for (const { label, pattern } of patterns) if (pattern.test(table)) return `${label} (AFP §6.5.2.1.3: ${pattern.source})`;
|
|
2142
2221
|
return null;
|
|
2143
2222
|
}
|
|
2223
|
+
//#endregion
|
|
2224
|
+
//#region src/albrecht/counter.ts
|
|
2225
|
+
/**
|
|
2226
|
+
* Assembles the count from the inventory.
|
|
2227
|
+
*
|
|
2228
|
+
* The order is not arbitrary: data functions depend on HOW transactions use
|
|
2229
|
+
* each store (AFP §6.5.4), and transactional functions depend on which stores
|
|
2230
|
+
* ended up counted. Hence: usage first, then the technical filter, then data,
|
|
2231
|
+
* then transactions.
|
|
2232
|
+
*/
|
|
2233
|
+
const RULESET = "afp";
|
|
2144
2234
|
/**
|
|
2145
2235
|
* Version of the rule set.
|
|
2146
2236
|
*
|
|
@@ -2149,14 +2239,18 @@ function isTechnical(store, patterns = DEFAULT_TECHNICAL_PATTERNS) {
|
|
|
2149
2239
|
* change rather than the work.
|
|
2150
2240
|
*
|
|
2151
2241
|
* It must be bumped by ANY change that moves the number for unchanged code, and
|
|
2152
|
-
* that is easy to forget
|
|
2242
|
+
* that is easy to forget. Four such changes landed in 1.1.0 — maintenance read
|
|
2153
2243
|
* across the whole project rather than from routes alone, a job followed into
|
|
2154
2244
|
* `process`, an event followed into its listeners, and `request.input(…)` counted
|
|
2155
|
-
* as a DET
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
2245
|
+
* as a DET — and three more in 1.2.0: an open input object counting 1 instead of 0,
|
|
2246
|
+
* `detFromSchema` no longer subtracting a placeholder that was not there, and a
|
|
2247
|
+
* write through `related(…)` maintaining the related table.
|
|
2248
|
+
*
|
|
2249
|
+
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
2250
|
+
* against this one and bills the tool's own improvement as work done. The guard
|
|
2251
|
+
* exists for exactly that, and only this constant arms it.
|
|
2158
2252
|
*/
|
|
2159
|
-
const RULESET_VERSION = "1.
|
|
2253
|
+
const RULESET_VERSION = "1.2.0";
|
|
2160
2254
|
function count(input, options = {}) {
|
|
2161
2255
|
const warnings = [];
|
|
2162
2256
|
const usage = usageOf(input);
|
|
@@ -2208,6 +2302,7 @@ function count(input, options = {}) {
|
|
|
2208
2302
|
});
|
|
2209
2303
|
warnings.push(...opaqueColumnWarnings(countable, input));
|
|
2210
2304
|
warnings.push(...unreadableInputWarnings(input));
|
|
2305
|
+
warnings.push(...openValidatorWarnings(input));
|
|
2211
2306
|
const functions = applyOverrides([...dataFunctions, ...transactionalFunctions], options.overrides ?? {}, input.jsonSchemas ?? /* @__PURE__ */ new Map(), tables, weights, warnings);
|
|
2212
2307
|
return {
|
|
2213
2308
|
ruleset: "afp",
|
|
@@ -2246,6 +2341,32 @@ function opaqueColumnWarnings(stores, input) {
|
|
|
2246
2341
|
return [`${found.length} opaque column(s), each counted as 1 DET. If the user recognises fields inside one, declare the count with \`overrides\` — counting-decisions §8:`, ...found];
|
|
2247
2342
|
}
|
|
2248
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}`);
|
|
2361
|
+
}
|
|
2362
|
+
if (found.length === 0) return [];
|
|
2363
|
+
return [
|
|
2364
|
+
`${found.length} open input object(s), each counted as 1 DET. The fields the user fills are data, not code, so this is a FLOOR: if they are declared anywhere in the source, name that schema with \`overrides.detFromSchema\` — counting-decisions §8:`,
|
|
2365
|
+
...found.slice(0, 10),
|
|
2366
|
+
...found.length > 10 ? [` … and ${found.length - 10} more`] : []
|
|
2367
|
+
];
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2249
2370
|
* Transactions that read the request in a way that enumerates nothing.
|
|
2250
2371
|
*
|
|
2251
2372
|
* `request.all()`, `request.body()`, `request.except([…])` — whatever arrives is
|
|
@@ -2319,8 +2440,20 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2319
2440
|
if (override.detFromSchema) {
|
|
2320
2441
|
const schema = schemas.get(override.detFromSchema);
|
|
2321
2442
|
if (schema) {
|
|
2322
|
-
|
|
2443
|
+
/**
|
|
2444
|
+
* Replaces the opaque placeholder — the one the rationale marks — rather
|
|
2445
|
+
* than assuming there is one and that it is worth 1.
|
|
2446
|
+
*
|
|
2447
|
+
* That assumption was wrong twice over. An open `vine.object` counted
|
|
2448
|
+
* ZERO, not 1, so subtracting 1 removed a field the analysis had read
|
|
2449
|
+
* correctly: 86 DETs where 87 was right. And a function with no opaque
|
|
2450
|
+
* DET at all was silently charged the subtraction too.
|
|
2451
|
+
*/
|
|
2452
|
+
const placeholders = fn.rationale.detSources.filter((source) => source.endsWith("(opaque)"));
|
|
2453
|
+
det = Math.max(fn.det - Math.min(placeholders.length, 1), 0) + schema.fields;
|
|
2323
2454
|
by = `config:overrides.${fn.name} (from ${schema.name}: ${schema.fields} fields)`;
|
|
2455
|
+
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
|
+
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.`);
|
|
2324
2457
|
} else warnings.push(`override for "${fn.name}" names schema "${override.detFromSchema}", which is not declared anywhere in the code: the DET count was left as found. A renamed or moved schema breaks the mapping, and this says so rather than counting on silently.`);
|
|
2325
2458
|
}
|
|
2326
2459
|
const refs = override.refs ?? fn.refs;
|
|
@@ -2524,6 +2657,13 @@ async function analyze(root, options = {}) {
|
|
|
2524
2657
|
by: "validator"
|
|
2525
2658
|
}
|
|
2526
2659
|
})),
|
|
2660
|
+
opaqueInputFields: behavior.opaqueInputFields.map((name) => ({
|
|
2661
|
+
name,
|
|
2662
|
+
provenance: {
|
|
2663
|
+
file: app.root,
|
|
2664
|
+
by: "validator"
|
|
2665
|
+
}
|
|
2666
|
+
})),
|
|
2527
2667
|
requestFields: behavior.requestFields.map((name) => ({
|
|
2528
2668
|
name,
|
|
2529
2669
|
provenance: {
|
|
@@ -2561,4 +2701,4 @@ async function analyze(root, options = {}) {
|
|
|
2561
2701
|
};
|
|
2562
2702
|
}
|
|
2563
2703
|
//#endregion
|
|
2564
|
-
export { analyze as n, CoverageTooLowError as t };
|
|
2704
|
+
export { RULESET_VERSION as i, analyze as n, RULESET as r, CoverageTooLowError as t };
|