@filipebraida/adonis-function-points 0.2.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 +105 -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-DLZP5bUp.js} +1 -1
- package/build/{fp_count-CfcXPuj5.js → fp_count-DNSwaLUD.js} +1 -1
- package/build/{fp_diff-DE_t3twv.js → fp_diff-CCKxqGKh.js} +1 -1
- package/build/{fp_explain-MKyEoi0h.js → fp_explain-Dpiby5Qx.js} +1 -1
- package/build/{fp_inventory-DSrCVhEy.js → fp_inventory-DHwZzEQf.js} +1 -1
- package/build/{fp_metrics-BUWj9dLw.js → fp_metrics-M84qLYaE.js} +1 -1
- package/build/index.d.ts +28 -0
- package/build/index.js +4 -3
- package/build/{pipeline-DySlMWcN.js → pipeline-Dm9KvUvF.js} +424 -48
- package/build/{resolvers-MFjRl2ef.js → resolvers-vMahHkAd.js} +356 -291
- package/build/{runners-DpMd-yZM.js → runners-DetZGfh5.js} +14 -388
- package/build/src/albrecht/counter.d.ts +9 -5
- 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/detectors/lucid.d.ts +8 -0
- package/build/src/inventory/graph/call_graph.d.ts +2 -0
- 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/src/types.d.ts +7 -0
- package/build/stubs/config.stub +7 -1
- package/package.json +1 -1
- package/build/define_config-DOqWyPwV.js +0 -19
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as rootSymbolOf, c as
|
|
2
|
-
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
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";
|
|
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,25 @@ 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
|
+
/**
|
|
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));
|
|
1259
|
+
const isOpaque = new Set(unreadable);
|
|
1260
|
+
for (const leaf of leaves) {
|
|
1261
|
+
const field = `${name}.${leaf}`;
|
|
1262
|
+
fields.push(field);
|
|
1263
|
+
if (isOpaque.has(leaf)) opaque.push(field);
|
|
1264
|
+
}
|
|
1237
1265
|
}
|
|
1238
|
-
return
|
|
1266
|
+
return {
|
|
1267
|
+
fields,
|
|
1268
|
+
opaque
|
|
1269
|
+
};
|
|
1239
1270
|
}
|
|
1240
1271
|
/**
|
|
1241
1272
|
* Fields read straight off the request, with no validator in between.
|
|
@@ -1320,26 +1351,139 @@ function findValidator(name, file, app) {
|
|
|
1320
1351
|
}
|
|
1321
1352
|
return null;
|
|
1322
1353
|
}
|
|
1323
|
-
/**
|
|
1324
|
-
|
|
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) {
|
|
1325
1402
|
const object = node.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1326
|
-
if (!object) return
|
|
1403
|
+
if (!object) return {
|
|
1404
|
+
leaves: [],
|
|
1405
|
+
opaque: []
|
|
1406
|
+
};
|
|
1327
1407
|
const leaves = [];
|
|
1408
|
+
const opaque = [];
|
|
1328
1409
|
const walk = (literal, prefix) => {
|
|
1329
1410
|
for (const property of literal.getProperties()) {
|
|
1330
1411
|
if (!Node.isPropertyAssignment(property)) continue;
|
|
1331
1412
|
const name = property.getName().replace(/['"]/g, "");
|
|
1332
|
-
const text = property.getText();
|
|
1333
1413
|
const nested = property.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression);
|
|
1334
|
-
if (nested &&
|
|
1335
|
-
|
|
1414
|
+
if (nested && isArgumentOfCallNamed(nested, "object")) {
|
|
1415
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
1416
|
+
/**
|
|
1417
|
+
* An object declaring no properties enumerates nothing. It is the field
|
|
1418
|
+
* itself that crosses the boundary, so it counts once — never zero,
|
|
1419
|
+
* which would make it cheaper than a plain string.
|
|
1420
|
+
*/
|
|
1421
|
+
if (nested.getProperties().length === 0) {
|
|
1422
|
+
leaves.push(path);
|
|
1423
|
+
opaque.push(path);
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
walk(nested, path);
|
|
1336
1427
|
continue;
|
|
1337
1428
|
}
|
|
1338
1429
|
leaves.push(prefix ? `${prefix}.${name}` : name);
|
|
1339
1430
|
}
|
|
1340
1431
|
};
|
|
1341
1432
|
walk(object, "");
|
|
1342
|
-
|
|
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
|
+
/**
|
|
1448
|
+
* The whole validator is an open object: nothing is enumerable, and the body
|
|
1449
|
+
* that carries it is measured at the floor. Counted as one, reported as such.
|
|
1450
|
+
*/
|
|
1451
|
+
if (leaves.length === 0 && object.getProperties().length === 0) return {
|
|
1452
|
+
leaves: ["*"],
|
|
1453
|
+
opaque: ["*"]
|
|
1454
|
+
};
|
|
1455
|
+
return {
|
|
1456
|
+
leaves: [...new Set(leaves)],
|
|
1457
|
+
opaque: [...new Set(opaque)]
|
|
1458
|
+
};
|
|
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;
|
|
1343
1487
|
}
|
|
1344
1488
|
/** file name, to identify the unresolved call without dumping the full path */
|
|
1345
1489
|
const pathOf = (file) => file.split("/").pop()?.replace(/\.ts$/, "") ?? file;
|
|
@@ -1440,7 +1584,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1440
1584
|
const accesses = [];
|
|
1441
1585
|
const followUps = [];
|
|
1442
1586
|
const unresolved = [];
|
|
1443
|
-
const
|
|
1587
|
+
const validator = validatorFieldsIn(body, file, app);
|
|
1444
1588
|
const request = requestFieldsIn(body);
|
|
1445
1589
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1446
1590
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
@@ -1449,9 +1593,14 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1449
1593
|
store: access.store,
|
|
1450
1594
|
write: access.mode === "write"
|
|
1451
1595
|
});
|
|
1596
|
+
/**
|
|
1597
|
+
* A relation reached by `preload`/`load` is read; one written through
|
|
1598
|
+
* `related('files').create(…)` is written. Assuming read either way made
|
|
1599
|
+
* a table maintained only through a relation come out as an EIF.
|
|
1600
|
+
*/
|
|
1452
1601
|
if (access.viaRelation) accesses.push({
|
|
1453
1602
|
store: access.viaRelation,
|
|
1454
|
-
write:
|
|
1603
|
+
write: access.relationWritten === true
|
|
1455
1604
|
});
|
|
1456
1605
|
/**
|
|
1457
1606
|
* counting-decisions §3: a hook belongs to the transaction that fired
|
|
@@ -1499,7 +1648,8 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1499
1648
|
accesses,
|
|
1500
1649
|
followUps,
|
|
1501
1650
|
unresolved,
|
|
1502
|
-
validators,
|
|
1651
|
+
validators: validator.fields,
|
|
1652
|
+
opaqueValidators: validator.opaque,
|
|
1503
1653
|
requestFields: request.fields,
|
|
1504
1654
|
opaqueRequest: request.opaque,
|
|
1505
1655
|
bodyHash: hashOf(body)
|
|
@@ -1527,7 +1677,15 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1527
1677
|
if (symbols.size === 0) continue;
|
|
1528
1678
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1529
1679
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
1530
|
-
if (access?.mode
|
|
1680
|
+
if (access?.mode !== "write") continue;
|
|
1681
|
+
written.add(access.store);
|
|
1682
|
+
/**
|
|
1683
|
+
* `distribution.related('files').create(…)` maintains the related table.
|
|
1684
|
+
* Recording only the parent left a table written exclusively that way
|
|
1685
|
+
* looking like somebody else's — reported by a production application as
|
|
1686
|
+
* an EIF it was sure it maintained.
|
|
1687
|
+
*/
|
|
1688
|
+
if (access.viaRelation && access.relationWritten) written.add(access.viaRelation);
|
|
1531
1689
|
}
|
|
1532
1690
|
}
|
|
1533
1691
|
return written;
|
|
@@ -1541,6 +1699,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1541
1699
|
function run(handler) {
|
|
1542
1700
|
const touches = /* @__PURE__ */ new Set();
|
|
1543
1701
|
const inputFields = /* @__PURE__ */ new Set();
|
|
1702
|
+
const opaqueInputFields = /* @__PURE__ */ new Set();
|
|
1544
1703
|
const requestFields = /* @__PURE__ */ new Set();
|
|
1545
1704
|
let opaqueRequest = false;
|
|
1546
1705
|
const trace = [];
|
|
@@ -1580,6 +1739,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1580
1739
|
}
|
|
1581
1740
|
unresolved.push(...facts.unresolved);
|
|
1582
1741
|
for (const field of facts.validators) inputFields.add(field);
|
|
1742
|
+
for (const field of facts.opaqueValidators) opaqueInputFields.add(field);
|
|
1583
1743
|
for (const field of facts.requestFields) requestFields.add(field);
|
|
1584
1744
|
if (facts.opaqueRequest) opaqueRequest = true;
|
|
1585
1745
|
trace.push({
|
|
@@ -1606,6 +1766,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1606
1766
|
writes,
|
|
1607
1767
|
touches: [...touches].sort(),
|
|
1608
1768
|
inputFields: [...inputFields].sort(),
|
|
1769
|
+
opaqueInputFields: [...opaqueInputFields].sort(),
|
|
1609
1770
|
requestFields: [...requestFields].sort(),
|
|
1610
1771
|
opaqueRequest,
|
|
1611
1772
|
trace,
|
|
@@ -1957,6 +2118,12 @@ function pointsOf(type, complexity, weights = DEFAULT_WEIGHTS) {
|
|
|
1957
2118
|
}
|
|
1958
2119
|
//#endregion
|
|
1959
2120
|
//#region src/albrecht/data_functions.ts
|
|
2121
|
+
/**
|
|
2122
|
+
* A column whose shape says nothing about what it holds. Marked in the rationale
|
|
2123
|
+
* because `detFromSchema` replaces exactly this placeholder, and because a reader
|
|
2124
|
+
* deserves to know which of the DETs is a floor rather than a count.
|
|
2125
|
+
*/
|
|
2126
|
+
const OPAQUE_TYPE$1 = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
1960
2127
|
function countDataFunctions(stores, usage, options) {
|
|
1961
2128
|
const counted = [];
|
|
1962
2129
|
for (const store of stores) {
|
|
@@ -1993,7 +2160,7 @@ function countDataFunctions(stores, usage, options) {
|
|
|
1993
2160
|
points: pointsOf(type, complexity, options.weights),
|
|
1994
2161
|
rationale: {
|
|
1995
2162
|
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}`),
|
|
2163
|
+
detSources: detAttributes.map((attribute) => `${store.columnSource}:${store.table ?? store.name}.${attribute.name}` + (attribute.type && OPAQUE_TYPE$1.test(attribute.type) ? " (opaque)" : "")),
|
|
1997
2164
|
refSources: options.retStrategy === "composition" ? ["1 (main group)", ...store.subgroups.map((s) => `composition:${s}`)] : ["1 (constant: a logical subgroup is not derivable from code)"]
|
|
1998
2165
|
}
|
|
1999
2166
|
});
|
|
@@ -2032,7 +2199,10 @@ function countTransactionalFunctions(entryPoints, behaviors, options) {
|
|
|
2032
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)",
|
|
2033
2200
|
detSources: sources,
|
|
2034
2201
|
refSources: touched.map((store) => `reaches:${store}`),
|
|
2035
|
-
trace: behavior.trace
|
|
2202
|
+
trace: behavior.trace.map((step) => ({
|
|
2203
|
+
...step,
|
|
2204
|
+
file: relativeTo(options.root, step.file)
|
|
2205
|
+
}))
|
|
2036
2206
|
}
|
|
2037
2207
|
});
|
|
2038
2208
|
}
|
|
@@ -2077,7 +2247,14 @@ function detsFor(entry, behavior, touched, type, options) {
|
|
|
2077
2247
|
sources.push(source);
|
|
2078
2248
|
};
|
|
2079
2249
|
for (const param of entry.signature.match(/:[A-Za-z_][\w]*/g) ?? []) add(param.slice(1), `param:${param}`);
|
|
2080
|
-
|
|
2250
|
+
/**
|
|
2251
|
+
* `(opaque)` is not decoration: `detFromSchema` replaces the opaque placeholder
|
|
2252
|
+
* with a schema's fields, and it used to assume there was exactly one and that
|
|
2253
|
+
* it was worth 1. An open `vine.object` counted zero, so the subtraction ate a
|
|
2254
|
+
* real field — off by one, in the direction of undercounting.
|
|
2255
|
+
*/
|
|
2256
|
+
const opaqueInputs = new Set(behavior.opaqueInputFields);
|
|
2257
|
+
for (const field of behavior.inputFields) add(field.split(".").pop(), `validator:${field}${opaqueInputs.has(field) ? " (opaque)" : ""}`);
|
|
2081
2258
|
/**
|
|
2082
2259
|
* §7.2 asks whether a user-recognisable field crosses the boundary, not how it
|
|
2083
2260
|
* was declared. `request.input('title')` does, and counted for nothing while
|
|
@@ -2141,6 +2318,17 @@ function isTechnical(store, patterns = DEFAULT_TECHNICAL_PATTERNS) {
|
|
|
2141
2318
|
for (const { label, pattern } of patterns) if (pattern.test(table)) return `${label} (AFP §6.5.2.1.3: ${pattern.source})`;
|
|
2142
2319
|
return null;
|
|
2143
2320
|
}
|
|
2321
|
+
//#endregion
|
|
2322
|
+
//#region src/albrecht/counter.ts
|
|
2323
|
+
/**
|
|
2324
|
+
* Assembles the count from the inventory.
|
|
2325
|
+
*
|
|
2326
|
+
* The order is not arbitrary: data functions depend on HOW transactions use
|
|
2327
|
+
* each store (AFP §6.5.4), and transactional functions depend on which stores
|
|
2328
|
+
* ended up counted. Hence: usage first, then the technical filter, then data,
|
|
2329
|
+
* then transactions.
|
|
2330
|
+
*/
|
|
2331
|
+
const RULESET = "afp";
|
|
2144
2332
|
/**
|
|
2145
2333
|
* Version of the rule set.
|
|
2146
2334
|
*
|
|
@@ -2149,14 +2337,18 @@ function isTechnical(store, patterns = DEFAULT_TECHNICAL_PATTERNS) {
|
|
|
2149
2337
|
* change rather than the work.
|
|
2150
2338
|
*
|
|
2151
2339
|
* It must be bumped by ANY change that moves the number for unchanged code, and
|
|
2152
|
-
* that is easy to forget
|
|
2340
|
+
* that is easy to forget. Four such changes landed in 1.1.0 — maintenance read
|
|
2153
2341
|
* across the whole project rather than from routes alone, a job followed into
|
|
2154
2342
|
* `process`, an event followed into its listeners, and `request.input(…)` counted
|
|
2155
|
-
* as a DET
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
2343
|
+
* as a DET — and three more in 1.2.0: an open input object counting 1 instead of 0,
|
|
2344
|
+
* `detFromSchema` no longer subtracting a placeholder that was not there, and a
|
|
2345
|
+
* write through `related(…)` maintaining the related table.
|
|
2346
|
+
*
|
|
2347
|
+
* Without the bump, a baseline saved by the previous version compares cleanly
|
|
2348
|
+
* against this one and bills the tool's own improvement as work done. The guard
|
|
2349
|
+
* exists for exactly that, and only this constant arms it.
|
|
2158
2350
|
*/
|
|
2159
|
-
const RULESET_VERSION = "1.
|
|
2351
|
+
const RULESET_VERSION = "1.3.0";
|
|
2160
2352
|
function count(input, options = {}) {
|
|
2161
2353
|
const warnings = [];
|
|
2162
2354
|
const usage = usageOf(input);
|
|
@@ -2202,13 +2394,38 @@ function count(input, options = {}) {
|
|
|
2202
2394
|
const ignored = new Set(options.boundary?.ignoreEntryPoints ?? []);
|
|
2203
2395
|
const transactionalFunctions = countTransactionalFunctions(input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? "")), input.behaviors, {
|
|
2204
2396
|
countedStores,
|
|
2397
|
+
root: input.app.root,
|
|
2205
2398
|
messageDet: options.messageDet ?? 0,
|
|
2206
2399
|
tables,
|
|
2207
2400
|
weights
|
|
2208
2401
|
});
|
|
2209
|
-
|
|
2210
|
-
|
|
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 ?? {});
|
|
2211
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));
|
|
2212
2429
|
return {
|
|
2213
2430
|
ruleset: "afp",
|
|
2214
2431
|
rulesetVersion: RULESET_VERSION,
|
|
@@ -2230,20 +2447,94 @@ function count(input, options = {}) {
|
|
|
2230
2447
|
* `metadata` column changes no number, and warning about it would be the noise
|
|
2231
2448
|
* that teaches people to stop reading the confidence block.
|
|
2232
2449
|
*/
|
|
2233
|
-
|
|
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) {
|
|
2234
2486
|
const reached = /* @__PURE__ */ new Map();
|
|
2235
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);
|
|
2236
|
-
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
|
+
};
|
|
2237
2499
|
for (const store of stores) {
|
|
2238
|
-
|
|
2239
|
-
if (!transactions) continue;
|
|
2500
|
+
if (!reached.get(store.name)) continue;
|
|
2240
2501
|
for (const attribute of store.attributes) {
|
|
2241
2502
|
if (!attribute.type || !OPAQUE_TYPE.test(attribute.type)) continue;
|
|
2242
|
-
|
|
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})`);
|
|
2243
2506
|
}
|
|
2244
2507
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
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);
|
|
2512
|
+
}
|
|
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;
|
|
2533
|
+
return [
|
|
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
|
|
2537
|
+
];
|
|
2247
2538
|
}
|
|
2248
2539
|
/**
|
|
2249
2540
|
* Transactions that read the request in a way that enumerates nothing.
|
|
@@ -2281,8 +2572,6 @@ function unreadableInputWarnings(input) {
|
|
|
2281
2572
|
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
2282
2573
|
];
|
|
2283
2574
|
}
|
|
2284
|
-
/** a column whose shape says nothing about what it holds */
|
|
2285
|
-
const OPAQUE_TYPE = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
2286
2575
|
/**
|
|
2287
2576
|
* Replaces what the analysis found with what a person declared.
|
|
2288
2577
|
*
|
|
@@ -2317,14 +2606,54 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2317
2606
|
* is born, not when a field is.
|
|
2318
2607
|
*/
|
|
2319
2608
|
if (override.detFromSchema) {
|
|
2320
|
-
|
|
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.`);
|
|
2321
2628
|
if (schema) {
|
|
2322
|
-
|
|
2629
|
+
/**
|
|
2630
|
+
* Replaces the opaque placeholder — the one the rationale marks — rather
|
|
2631
|
+
* than assuming there is one and that it is worth 1.
|
|
2632
|
+
*
|
|
2633
|
+
* That assumption was wrong twice over. An open `vine.object` counted
|
|
2634
|
+
* ZERO, not 1, so subtracting 1 removed a field the analysis had read
|
|
2635
|
+
* correctly: 86 DETs where 87 was right. And a function with no opaque
|
|
2636
|
+
* DET at all was silently charged the subtraction too.
|
|
2637
|
+
*/
|
|
2638
|
+
const placeholders = fn.rationale.detSources.filter((source) => source.endsWith("(opaque)"));
|
|
2639
|
+
det = Math.max(fn.det - Math.min(placeholders.length, 1), 0) + schema.fields;
|
|
2323
2640
|
by = `config:overrides.${fn.name} (from ${schema.name}: ${schema.fields} fields)`;
|
|
2324
|
-
|
|
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.`);
|
|
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.`);
|
|
2643
|
+
}
|
|
2325
2644
|
}
|
|
2326
2645
|
const refs = override.refs ?? fn.refs;
|
|
2327
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;
|
|
2328
2657
|
return {
|
|
2329
2658
|
...fn,
|
|
2330
2659
|
det,
|
|
@@ -2468,7 +2797,7 @@ function describeSource(root, config) {
|
|
|
2468
2797
|
]),
|
|
2469
2798
|
dirty: status === void 0 ? void 0 : status.length > 0,
|
|
2470
2799
|
countedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2471
|
-
config: config === null ? null :
|
|
2800
|
+
config: config === null ? null : relativeTo(root, config)
|
|
2472
2801
|
};
|
|
2473
2802
|
}
|
|
2474
2803
|
function appName(root) {
|
|
@@ -2502,17 +2831,51 @@ async function analyze(root, options = {}) {
|
|
|
2502
2831
|
const behaviors = new Map(entryPoints.filter((entry) => entry.handler).map((entry) => [entry.id, analyzer.analyze(entry.handler)]));
|
|
2503
2832
|
const resolved = [...behaviors.values()].filter((behavior) => behavior.unresolved.length === 0).length;
|
|
2504
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
|
+
});
|
|
2505
2853
|
const inventory = {
|
|
2506
2854
|
version: 1,
|
|
2507
2855
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2508
|
-
app: app
|
|
2856
|
+
app: source.app,
|
|
2509
2857
|
framework: {
|
|
2510
2858
|
core: app.framework.core,
|
|
2511
2859
|
lucid: app.framework.lucid,
|
|
2512
2860
|
orm: app.framework.orm
|
|
2513
2861
|
},
|
|
2514
|
-
dataStores: stores
|
|
2515
|
-
|
|
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
|
+
})),
|
|
2516
2879
|
behaviors: [...behaviors.entries()].map(([entryPointId, behavior]) => ({
|
|
2517
2880
|
entryPointId,
|
|
2518
2881
|
writes: behavior.writes,
|
|
@@ -2520,21 +2883,34 @@ async function analyze(root, options = {}) {
|
|
|
2520
2883
|
inputFields: behavior.inputFields.map((name) => ({
|
|
2521
2884
|
name,
|
|
2522
2885
|
provenance: {
|
|
2523
|
-
file: app.root,
|
|
2886
|
+
file: emit(app.root),
|
|
2887
|
+
by: "validator"
|
|
2888
|
+
}
|
|
2889
|
+
})),
|
|
2890
|
+
opaqueInputFields: behavior.opaqueInputFields.map((name) => ({
|
|
2891
|
+
name,
|
|
2892
|
+
provenance: {
|
|
2893
|
+
file: emit(app.root),
|
|
2524
2894
|
by: "validator"
|
|
2525
2895
|
}
|
|
2526
2896
|
})),
|
|
2527
2897
|
requestFields: behavior.requestFields.map((name) => ({
|
|
2528
2898
|
name,
|
|
2529
2899
|
provenance: {
|
|
2530
|
-
file: app.root,
|
|
2900
|
+
file: emit(app.root),
|
|
2531
2901
|
by: "request"
|
|
2532
2902
|
}
|
|
2533
2903
|
})),
|
|
2534
2904
|
opaqueRequest: behavior.opaqueRequest,
|
|
2535
2905
|
outputFields: [],
|
|
2536
|
-
trace: behavior.trace
|
|
2537
|
-
|
|
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
|
+
}))
|
|
2538
2914
|
})),
|
|
2539
2915
|
coverage: {
|
|
2540
2916
|
entryPointsTotal: entryPoints.length,
|
|
@@ -2556,9 +2932,9 @@ async function analyze(root, options = {}) {
|
|
|
2556
2932
|
jsonSchemas,
|
|
2557
2933
|
writtenAnywhere: analyzer.writtenAnywhere()
|
|
2558
2934
|
}, options),
|
|
2559
|
-
source
|
|
2935
|
+
source
|
|
2560
2936
|
}
|
|
2561
2937
|
};
|
|
2562
2938
|
}
|
|
2563
2939
|
//#endregion
|
|
2564
|
-
export { analyze as n, CoverageTooLowError as t };
|
|
2940
|
+
export { RULESET_VERSION as i, analyze as n, RULESET as r, CoverageTooLowError as t };
|