@filipebraida/adonis-function-points 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/README.md +17 -6
- package/build/commands/main.js +6 -6
- package/build/{fp_calibrate-iFAec0tA.js → fp_calibrate-DUbHiifm.js} +1 -1
- package/build/{fp_count-D21tQ_pv.js → fp_count-ChtblhZV.js} +1 -1
- package/build/{fp_diff-D0pHGMgi.js → fp_diff-Dt7J4IWu.js} +1 -1
- package/build/{fp_explain-BwFs-LW-.js → fp_explain-DZJ--0-S.js} +1 -1
- package/build/{fp_inventory-Bu6O1Nn0.js → fp_inventory-CPtmuuke.js} +1 -1
- package/build/{fp_metrics-MGDppfSa.js → fp_metrics-et8F1Wvt.js} +1 -1
- package/build/index.js +2 -2
- package/build/{pipeline-CIAydCcT.js → pipeline-CNTBhs6o.js} +442 -79
- package/build/{resolvers-CRB6lXoo.js → resolvers-PJwo2Z8R.js} +64 -1
- package/build/{runners-CmxNHuuq.js → runners-DIt1G85i.js} +19 -4
- 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/graph/call_graph.d.ts +10 -0
- package/build/src/inventory/paths.d.ts +18 -0
- package/build/src/inventory/resolvers/index.d.ts +7 -0
- package/build/src/inventory/resolvers/index.js +2 -2
- package/build/src/inventory/resolvers/types.d.ts +19 -0
- package/build/src/inventory/source.d.ts +8 -1
- package/build/src/pipeline.js +1 -1
- package/build/src/types.d.ts +2 -0
- package/build/stubs/config.stub +7 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as hooksFiredBy, c as isApplicationCode, d as toPosix, i as detectAccess, l as relativeTo, n as isTechnicalWrite, o as rootSymbolOf, r as resolveCall, s as collectEventBindings, t as BUILTIN_CALL_RESOLVERS, u as samePath } from "./resolvers-PJwo2Z8R.js";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
@@ -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;
|
|
@@ -1491,12 +1586,34 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1491
1586
|
const unresolved = [];
|
|
1492
1587
|
const validator = validatorFieldsIn(body, file, app);
|
|
1493
1588
|
const request = requestFieldsIn(body);
|
|
1589
|
+
const context = {
|
|
1590
|
+
file,
|
|
1591
|
+
depth: 0,
|
|
1592
|
+
imports,
|
|
1593
|
+
exportedAs,
|
|
1594
|
+
injected,
|
|
1595
|
+
eventBindings,
|
|
1596
|
+
dataStoresBySymbol: storesByName,
|
|
1597
|
+
resolveSpecifier: app.resolveSpecifier,
|
|
1598
|
+
sourceFile
|
|
1599
|
+
};
|
|
1494
1600
|
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
1495
1601
|
const access = detectAccess(call, symbols, relationsByStore);
|
|
1496
1602
|
if (access) {
|
|
1603
|
+
/**
|
|
1604
|
+
* Asked about a DIRECT write too, not only about a call a resolver follows.
|
|
1605
|
+
*
|
|
1606
|
+
* `Notification.query().…update({ status: 'read' })` in a `show` handler is
|
|
1607
|
+
* exactly the shape this exists for, and the write IS the call — there is no
|
|
1608
|
+
* method to declare. Asking in one place only would have covered the service
|
|
1609
|
+
* call and missed the query builder beside it, which is the same fact written
|
|
1610
|
+
* differently.
|
|
1611
|
+
*/
|
|
1612
|
+
const technical = access.mode === "write" && isTechnicalWrite(call, context, resolvers);
|
|
1497
1613
|
accesses.push({
|
|
1498
1614
|
store: access.store,
|
|
1499
|
-
write: access.mode === "write"
|
|
1615
|
+
write: access.mode === "write",
|
|
1616
|
+
technical
|
|
1500
1617
|
});
|
|
1501
1618
|
/**
|
|
1502
1619
|
* A relation reached by `preload`/`load` is read; one written through
|
|
@@ -1524,21 +1641,18 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1524
1641
|
* shape must not be CLAIMED, not merely not reported.
|
|
1525
1642
|
*/
|
|
1526
1643
|
if (isIterationCall(call)) continue;
|
|
1527
|
-
const resolved = resolveCall(call,
|
|
1528
|
-
file,
|
|
1529
|
-
depth: 0,
|
|
1530
|
-
imports,
|
|
1531
|
-
exportedAs,
|
|
1532
|
-
injected,
|
|
1533
|
-
eventBindings,
|
|
1534
|
-
dataStoresBySymbol: storesByName,
|
|
1535
|
-
resolveSpecifier: app.resolveSpecifier,
|
|
1536
|
-
sourceFile
|
|
1537
|
-
}, resolvers);
|
|
1644
|
+
const resolved = resolveCall(call, context, resolvers);
|
|
1538
1645
|
if (resolved) {
|
|
1646
|
+
/**
|
|
1647
|
+
* Declared about the CALL, so everything reached through it is incidental too:
|
|
1648
|
+
* `persistOrganizationVisit` is called from several screens and saying it once
|
|
1649
|
+
* covers all of them.
|
|
1650
|
+
*/
|
|
1651
|
+
const technical = isTechnicalWrite(call, context, resolvers);
|
|
1539
1652
|
for (const next of resolved.refs) followUps.push({
|
|
1540
1653
|
ref: next,
|
|
1541
|
-
by: resolved.by
|
|
1654
|
+
by: resolved.by,
|
|
1655
|
+
technical
|
|
1542
1656
|
});
|
|
1543
1657
|
continue;
|
|
1544
1658
|
}
|
|
@@ -1578,6 +1692,17 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1578
1692
|
const writtenAnywhere = () => {
|
|
1579
1693
|
const written = /* @__PURE__ */ new Set();
|
|
1580
1694
|
for (const file of project.getSourceFiles()) {
|
|
1695
|
+
/**
|
|
1696
|
+
* A seeder's inserts are not the application maintaining a table, and a test
|
|
1697
|
+
* factory's are not either. Counting them made every reference table an ILF:
|
|
1698
|
+
* the CPM puts data maintained by the development team at an EIF at most, and
|
|
1699
|
+
* code data outside the count entirely.
|
|
1700
|
+
*
|
|
1701
|
+
* This is the same notion `scanRoots` applies at the root, applied at any
|
|
1702
|
+
* depth — because a domain-module layout puts `tests/` and `seeders/` inside
|
|
1703
|
+
* `app/`, where the root filter never looks.
|
|
1704
|
+
*/
|
|
1705
|
+
if (!isApplicationCode(app.root, file.getFilePath())) continue;
|
|
1581
1706
|
const symbols = storeSymbolsFor(file, file, app, storesByName);
|
|
1582
1707
|
if (symbols.size === 0) continue;
|
|
1583
1708
|
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
@@ -1603,6 +1728,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1603
1728
|
};
|
|
1604
1729
|
function run(handler) {
|
|
1605
1730
|
const touches = /* @__PURE__ */ new Set();
|
|
1731
|
+
const writtenStores = /* @__PURE__ */ new Set();
|
|
1606
1732
|
const inputFields = /* @__PURE__ */ new Set();
|
|
1607
1733
|
const opaqueInputFields = /* @__PURE__ */ new Set();
|
|
1608
1734
|
const requestFields = /* @__PURE__ */ new Set();
|
|
@@ -1612,7 +1738,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1612
1738
|
const unresolved = [];
|
|
1613
1739
|
const visited = /* @__PURE__ */ new Set();
|
|
1614
1740
|
let writes = false;
|
|
1615
|
-
const visit = (ref, depth) => {
|
|
1741
|
+
const visit = (ref, depth, technical = false) => {
|
|
1616
1742
|
const key = `${ref.file}#${ref.member ?? ref.line ?? "*"}`;
|
|
1617
1743
|
if (visited.has(key) || depth > maxDepth) return;
|
|
1618
1744
|
visited.add(key);
|
|
@@ -1637,10 +1763,16 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1637
1763
|
let bodyWrites = false;
|
|
1638
1764
|
for (const access of facts.accesses) {
|
|
1639
1765
|
touches.add(access.store);
|
|
1640
|
-
if (access.write)
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1766
|
+
if (!access.write) continue;
|
|
1767
|
+
/**
|
|
1768
|
+
* The store is maintained either way — a visit table really is written by this
|
|
1769
|
+
* application, so it stays an ILF and stays an FTR. What a technical write does
|
|
1770
|
+
* not do is decide what the transaction is FOR: §6.5.3 would read a `GET` that
|
|
1771
|
+
* notes the visit as an EI, and the CPM asks about primary intent.
|
|
1772
|
+
*/
|
|
1773
|
+
writtenStores.add(access.store);
|
|
1774
|
+
bodyWrites = true;
|
|
1775
|
+
if (!technical && !access.technical) writes = true;
|
|
1644
1776
|
}
|
|
1645
1777
|
unresolved.push(...facts.unresolved);
|
|
1646
1778
|
for (const field of facts.validators) inputFields.add(field);
|
|
@@ -1662,7 +1794,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1662
1794
|
if (depth >= maxDepth) return;
|
|
1663
1795
|
for (const followUp of facts.followUps) {
|
|
1664
1796
|
const before = trace.length;
|
|
1665
|
-
visit(followUp.ref, depth + 1);
|
|
1797
|
+
visit(followUp.ref, depth + 1, technical || followUp.technical === true);
|
|
1666
1798
|
if (trace.length > before) trace[before].by = followUp.by;
|
|
1667
1799
|
}
|
|
1668
1800
|
};
|
|
@@ -1670,6 +1802,7 @@ function createAnalyzer(app, stores, options = {}) {
|
|
|
1670
1802
|
return {
|
|
1671
1803
|
writes,
|
|
1672
1804
|
touches: [...touches].sort(),
|
|
1805
|
+
writtenStores: [...writtenStores].sort(),
|
|
1673
1806
|
inputFields: [...inputFields].sort(),
|
|
1674
1807
|
opaqueInputFields: [...opaqueInputFields].sort(),
|
|
1675
1808
|
requestFields: [...requestFields].sort(),
|
|
@@ -2104,7 +2237,10 @@ function countTransactionalFunctions(entryPoints, behaviors, options) {
|
|
|
2104
2237
|
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
2238
|
detSources: sources,
|
|
2106
2239
|
refSources: touched.map((store) => `reaches:${store}`),
|
|
2107
|
-
trace: behavior.trace
|
|
2240
|
+
trace: behavior.trace.map((step) => ({
|
|
2241
|
+
...step,
|
|
2242
|
+
file: relativeTo(options.root, step.file)
|
|
2243
|
+
}))
|
|
2108
2244
|
}
|
|
2109
2245
|
});
|
|
2110
2246
|
}
|
|
@@ -2250,7 +2386,7 @@ const RULESET = "afp";
|
|
|
2250
2386
|
* against this one and bills the tool's own improvement as work done. The guard
|
|
2251
2387
|
* exists for exactly that, and only this constant arms it.
|
|
2252
2388
|
*/
|
|
2253
|
-
const RULESET_VERSION = "1.
|
|
2389
|
+
const RULESET_VERSION = "1.4.0";
|
|
2254
2390
|
function count(input, options = {}) {
|
|
2255
2391
|
const warnings = [];
|
|
2256
2392
|
const usage = usageOf(input);
|
|
@@ -2279,7 +2415,17 @@ function count(input, options = {}) {
|
|
|
2279
2415
|
* quietly counting one more store.
|
|
2280
2416
|
*/
|
|
2281
2417
|
if (business.has(store.name) || business.has(store.table ?? "")) {
|
|
2282
|
-
|
|
2418
|
+
/**
|
|
2419
|
+
* Whether any transaction WRITES it, said out loud.
|
|
2420
|
+
*
|
|
2421
|
+
* `business` is meant for data the user maintains, and it accepted without
|
|
2422
|
+
* comment a table nothing in the application writes. That is how a team put two
|
|
2423
|
+
* read-only lookup tables in it believing they had a CRUD — the routes were
|
|
2424
|
+
* `.only(['index', 'show'])`. The declaration is still honoured, because only a
|
|
2425
|
+
* person knows, but the fact that contradicts it is now in the report.
|
|
2426
|
+
*/
|
|
2427
|
+
const maintained = usage.get(store.name)?.written === true || (input.writtenAnywhere?.has(store.name) ?? false);
|
|
2428
|
+
warnings.push(`kept by boundary configuration: ${store.name} — the AFP naming filter had excluded it (${technical})` + (maintained ? "" : ". NOTE: no transaction of this application writes it, so it counts as an EIF — check that the screens for it are more than index and show"));
|
|
2283
2429
|
return true;
|
|
2284
2430
|
}
|
|
2285
2431
|
warnings.push(`technical, excluded: ${store.name} (${technical})`);
|
|
@@ -2296,14 +2442,39 @@ function count(input, options = {}) {
|
|
|
2296
2442
|
const ignored = new Set(options.boundary?.ignoreEntryPoints ?? []);
|
|
2297
2443
|
const transactionalFunctions = countTransactionalFunctions(input.entryPoints.filter((entry) => !ignored.has(entry.identity) && !ignored.has(entry.name ?? "")), input.behaviors, {
|
|
2298
2444
|
countedStores,
|
|
2445
|
+
root: input.app.root,
|
|
2299
2446
|
messageDet: options.messageDet ?? 0,
|
|
2300
2447
|
tables,
|
|
2301
2448
|
weights
|
|
2302
2449
|
});
|
|
2303
|
-
|
|
2450
|
+
/**
|
|
2451
|
+
* `opaqueReviewed` answers a warning that is CORRECT and therefore permanent.
|
|
2452
|
+
*
|
|
2453
|
+
* 1 DET for an opaque column is a floor, and `fp:count` says so on every run. But
|
|
2454
|
+
* some of those columns are one field — a copy, a checksum, a bag of metadata —
|
|
2455
|
+
* and there was no way to record that someone had looked. A warning that cannot be
|
|
2456
|
+
* answered is one the team learns to scroll past, which costs more than the warning
|
|
2457
|
+
* reports. It silences nothing else: the count does not move, and how many were
|
|
2458
|
+
* reviewed is still printed.
|
|
2459
|
+
*/
|
|
2460
|
+
const reviewed = reviewedOpaque(options.overrides ?? {});
|
|
2461
|
+
const functions = applyOverrides([...dataFunctions, ...transactionalFunctions], options.overrides ?? {}, input.jsonSchemas ?? /* @__PURE__ */ new Map(), tables, weights, warnings, reviewed);
|
|
2462
|
+
/**
|
|
2463
|
+
* Reported AFTER the overrides are applied, because the overrides are the answer
|
|
2464
|
+
* to it.
|
|
2465
|
+
*
|
|
2466
|
+
* Computed first, the list kept naming functions whose floor had already been
|
|
2467
|
+
* replaced by `detFromSchema` — telling the reader to go and map something that
|
|
2468
|
+
* was mapped. It cost a real misreading: a report was taken as "two forms still
|
|
2469
|
+
* unmapped" when both were declared, by whoever wrote this code.
|
|
2470
|
+
*/
|
|
2471
|
+
const declared = new Set(functions.filter((fn) => fn.rationale.overrides?.some((o) => o.fields.includes("det"))).map((fn) => fn.name));
|
|
2472
|
+
warnings.push(...opaqueWarnings(countable, input, {
|
|
2473
|
+
reviewed,
|
|
2474
|
+
declared,
|
|
2475
|
+
overrides: options.overrides ?? {}
|
|
2476
|
+
}));
|
|
2304
2477
|
warnings.push(...unreadableInputWarnings(input));
|
|
2305
|
-
warnings.push(...openValidatorWarnings(input));
|
|
2306
|
-
const functions = applyOverrides([...dataFunctions, ...transactionalFunctions], options.overrides ?? {}, input.jsonSchemas ?? /* @__PURE__ */ new Map(), tables, weights, warnings);
|
|
2307
2478
|
return {
|
|
2308
2479
|
ruleset: "afp",
|
|
2309
2480
|
rulesetVersion: RULESET_VERSION,
|
|
@@ -2325,45 +2496,147 @@ function count(input, options = {}) {
|
|
|
2325
2496
|
* `metadata` column changes no number, and warning about it would be the noise
|
|
2326
2497
|
* that teaches people to stop reading the confidence block.
|
|
2327
2498
|
*/
|
|
2328
|
-
|
|
2499
|
+
/**
|
|
2500
|
+
* Opaque DETs someone has declared reviewed, in both spellings a person might use.
|
|
2501
|
+
*
|
|
2502
|
+
* Qualified (`Petition.schema`) is unambiguous; bare (`schema`) is what someone reads
|
|
2503
|
+
* off the warning line.
|
|
2504
|
+
*/
|
|
2505
|
+
function reviewedOpaque(overrides) {
|
|
2506
|
+
const reviewed = /* @__PURE__ */ new Set();
|
|
2507
|
+
for (const [name, override] of Object.entries(overrides)) for (const entry of override.opaqueReviewed ?? []) {
|
|
2508
|
+
reviewed.add(entry);
|
|
2509
|
+
reviewed.add(`${name}.${entry}`);
|
|
2510
|
+
/**
|
|
2511
|
+
* The bare field too, because the two sides of this comparison spell things
|
|
2512
|
+
* differently. `opaqueReviewed` is written against the FUNCTION (`Petition.schema`)
|
|
2513
|
+
* while a rationale source carries the TABLE (`ast:petitions.schema`), and the
|
|
2514
|
+
* qualified form cannot be recovered from either. The last segment is what they
|
|
2515
|
+
* share, and without it a correctly written review matched the count and not the
|
|
2516
|
+
* rationale — so `fp:explain` showed no review and the override warning still
|
|
2517
|
+
* claimed five unanswered floors.
|
|
2518
|
+
*/
|
|
2519
|
+
reviewed.add(entry.split(".").pop() ?? entry);
|
|
2520
|
+
}
|
|
2521
|
+
return reviewed;
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* The identity of an opaque DET inside a rationale source.
|
|
2525
|
+
*
|
|
2526
|
+
* `ast:petitions.schema (opaque)` is the store's TABLE name, and `opaqueReviewed` is
|
|
2527
|
+
* written against the FUNCTION name (`Petition.schema`), so the qualified form cannot
|
|
2528
|
+
* be recovered from the source alone — the bare field is what both sides share.
|
|
2529
|
+
*/
|
|
2530
|
+
const opaqueNameOf = (source) => source.replace(/^[a-z-]+:/, "").replace(/ \(opaque.*\)$/, "");
|
|
2531
|
+
const shortOpaqueNameOf = (source) => opaqueNameOf(source).split(".").pop() ?? "";
|
|
2532
|
+
/** `fp:explain` should say which floors someone has already looked at */
|
|
2533
|
+
function markReviewed(sources, reviewed) {
|
|
2534
|
+
return sources.map((source) => {
|
|
2535
|
+
if (!source.endsWith("(opaque)")) return source;
|
|
2536
|
+
return reviewed.has(opaqueNameOf(source)) || reviewed.has(shortOpaqueNameOf(source)) ? source.replace("(opaque)", "(opaque, reviewed)") : source;
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
/** a column whose shape says nothing about what it holds */
|
|
2540
|
+
const OPAQUE_TYPE = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
2541
|
+
/**
|
|
2542
|
+
* DETs the analysis cannot read: an opaque column, or an open input object.
|
|
2543
|
+
*
|
|
2544
|
+
* Both count 1, which is a FLOOR rather than a measurement, and counting-decisions §8
|
|
2545
|
+
* is the trade. Reporting it is the point — this was the one known blind spot the
|
|
2546
|
+
* package reported nowhere.
|
|
2547
|
+
*
|
|
2548
|
+
* Grouped by FUNCTION and stating what has already been answered, because a flat list
|
|
2549
|
+
* of columns could not say that. Computed before the overrides ran, it named functions
|
|
2550
|
+
* whose floor `detFromSchema` had already replaced, which reads as "go and map this"
|
|
2551
|
+
* about something already mapped. That misreading actually happened, to the author of
|
|
2552
|
+
* this code, reading someone else's report.
|
|
2553
|
+
*
|
|
2554
|
+
* Three states per function, and only the third is a request to do something:
|
|
2555
|
+
*
|
|
2556
|
+
* replaced a `detFromSchema` override stands in for one of them
|
|
2557
|
+
* reviewed someone looked and 1 is the right answer
|
|
2558
|
+
* floor still unanswered
|
|
2559
|
+
*/
|
|
2560
|
+
function opaqueWarnings(stores, input, state) {
|
|
2329
2561
|
const reached = /* @__PURE__ */ new Map();
|
|
2330
2562
|
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
|
|
2563
|
+
const byFunction = /* @__PURE__ */ new Map();
|
|
2564
|
+
const tally = (name, kind, transactions) => {
|
|
2565
|
+
const found = byFunction.get(name) ?? {
|
|
2566
|
+
kind,
|
|
2567
|
+
floor: [],
|
|
2568
|
+
reviewed: 0,
|
|
2569
|
+
reviewedNames: [],
|
|
2570
|
+
transactions
|
|
2571
|
+
};
|
|
2572
|
+
byFunction.set(name, found);
|
|
2573
|
+
return found;
|
|
2574
|
+
};
|
|
2332
2575
|
for (const store of stores) {
|
|
2333
|
-
|
|
2334
|
-
if (!transactions) continue;
|
|
2576
|
+
if (!reached.get(store.name)) continue;
|
|
2335
2577
|
for (const attribute of store.attributes) {
|
|
2336
2578
|
if (!attribute.type || !OPAQUE_TYPE.test(attribute.type)) continue;
|
|
2337
|
-
|
|
2579
|
+
const entry = tally(store.name, "column", reached.get(store.name) ?? 0);
|
|
2580
|
+
if (state.reviewed.has(`${store.name}.${attribute.name}`) || state.reviewed.has(attribute.name)) {
|
|
2581
|
+
entry.reviewed += 1;
|
|
2582
|
+
entry.reviewedNames.push(attribute.name);
|
|
2583
|
+
} else entry.floor.push(`${attribute.name} (${attribute.type})`);
|
|
2338
2584
|
}
|
|
2339
2585
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
*
|
|
2349
|
-
*
|
|
2350
|
-
*
|
|
2351
|
-
*
|
|
2352
|
-
*
|
|
2353
|
-
*
|
|
2354
|
-
|
|
2355
|
-
*/
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2586
|
+
for (const point of input.entryPoints) for (const field of input.behaviors.get(point.id)?.opaqueInputFields ?? []) {
|
|
2587
|
+
const entry = tally(point.identity, "input object", 1);
|
|
2588
|
+
if (state.reviewed.has(field) || state.reviewed.has(`${point.identity}.${field}`)) {
|
|
2589
|
+
entry.reviewed += 1;
|
|
2590
|
+
entry.reviewedNames.push(field);
|
|
2591
|
+
} else entry.floor.push(field);
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* A review that matches nothing is a review that does nothing.
|
|
2595
|
+
*
|
|
2596
|
+
* `detFromSchema` already warns when it names a schema that is not declared, and
|
|
2597
|
+
* `opaqueReviewed` did not — so `['messages.schema']` against a field actually named
|
|
2598
|
+
* `createMessageValidator.messages.schema` reviewed nothing in silence while the
|
|
2599
|
+
* warning kept firing, which reads as the tool ignoring the configuration.
|
|
2600
|
+
*/
|
|
2601
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2602
|
+
for (const [name, entry] of byFunction) for (const field of [...entry.floor, ...entry.reviewedNames]) {
|
|
2603
|
+
const bare = field.replace(/ \(.*\)$/, "");
|
|
2604
|
+
seen.add(bare);
|
|
2605
|
+
seen.add(`${name}.${bare}`);
|
|
2606
|
+
seen.add(bare.split(".").pop() ?? bare);
|
|
2361
2607
|
}
|
|
2362
|
-
|
|
2608
|
+
const unmatched = [];
|
|
2609
|
+
for (const [name, override] of Object.entries(state.overrides)) for (const declaredName of override.opaqueReviewed ?? []) if (![
|
|
2610
|
+
declaredName,
|
|
2611
|
+
`${name}.${declaredName}`,
|
|
2612
|
+
declaredName.split(".").pop() ?? ""
|
|
2613
|
+
].some((spelling) => seen.has(spelling))) unmatched.push(`${name}.opaqueReviewed: ${declaredName}`);
|
|
2614
|
+
const lines = [];
|
|
2615
|
+
let answered = 0;
|
|
2616
|
+
for (const [name, entry] of byFunction) {
|
|
2617
|
+
/** a declared schema stands in for exactly one placeholder — §8, and the override warns when there are more */
|
|
2618
|
+
const replaced = state.declared.has(name) && entry.floor.length > 0 ? 1 : 0;
|
|
2619
|
+
const remaining = entry.floor.slice(replaced);
|
|
2620
|
+
if (remaining.length === 0) {
|
|
2621
|
+
answered += 1;
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
const answeredHere = [...replaced > 0 ? [`${replaced} replaced by override`] : [], ...entry.reviewed > 0 ? [`${entry.reviewed} reviewed`] : []];
|
|
2625
|
+
/**
|
|
2626
|
+
* How many transactions reach the store, so the reader can judge whether the
|
|
2627
|
+
* floor is worth answering. A blob nothing touches changes no number.
|
|
2628
|
+
*/
|
|
2629
|
+
const reach = entry.kind === "column" ? `, reached by ${entry.transactions} transaction(s)` : "";
|
|
2630
|
+
lines.push(` ${name} — ${remaining.length} ${entry.kind}(s) at 1 DET${reach}` + (answeredHere.length > 0 ? ` (${answeredHere.join(", ")} already)` : "") + `: ${remaining.map((f) => entry.kind === "column" ? `${name}.${f}` : f).join(", ")}`);
|
|
2631
|
+
}
|
|
2632
|
+
const settled = answered === 0 ? [] : [` (${answered} more function(s) whose opaque DETs are all accounted for)`];
|
|
2633
|
+
const unmatchedLines = unmatched.length === 0 ? [] : [`${unmatched.length} \`opaqueReviewed\` entr(ies) match no opaque DET, so they review nothing. The name is the one the count prints:`, ...unmatched.map((u) => ` ${u}`)];
|
|
2634
|
+
if (lines.length === 0) return [...unmatchedLines, ...settled];
|
|
2363
2635
|
return [
|
|
2364
|
-
`${
|
|
2365
|
-
...
|
|
2366
|
-
...
|
|
2636
|
+
`${lines.length} function(s) with a DET the analysis cannot read, counted as 1 each — a FLOOR, not a measurement. Where the fields are declared in the source, name that schema with \`overrides.detFromSchema\`; where 1 is the right answer, record it with \`overrides.<fn>.opaqueReviewed\` — counting-decisions §8:`,
|
|
2637
|
+
...lines,
|
|
2638
|
+
...settled,
|
|
2639
|
+
...unmatchedLines
|
|
2367
2640
|
];
|
|
2368
2641
|
}
|
|
2369
2642
|
/**
|
|
@@ -2402,8 +2675,6 @@ function unreadableInputWarnings(input) {
|
|
|
2402
2675
|
...blind.length > 10 ? [` … and ${blind.length - 10} more`] : []
|
|
2403
2676
|
];
|
|
2404
2677
|
}
|
|
2405
|
-
/** a column whose shape says nothing about what it holds */
|
|
2406
|
-
const OPAQUE_TYPE = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
2407
2678
|
/**
|
|
2408
2679
|
* Replaces what the analysis found with what a person declared.
|
|
2409
2680
|
*
|
|
@@ -2415,7 +2686,7 @@ const OPAQUE_TYPE = /^(object|any|unknown|Record<|Json|JSON)/;
|
|
|
2415
2686
|
* An override naming no function is a warning, never silence: a typo in the key
|
|
2416
2687
|
* would otherwise mean the declaration did nothing and nobody was told.
|
|
2417
2688
|
*/
|
|
2418
|
-
function applyOverrides(functions, overrides, schemas, tables, weights, warnings) {
|
|
2689
|
+
function applyOverrides(functions, overrides, schemas, tables, weights, warnings, reviewed) {
|
|
2419
2690
|
const keys = Object.keys(overrides);
|
|
2420
2691
|
if (keys.length === 0) return functions;
|
|
2421
2692
|
const used = /* @__PURE__ */ new Set();
|
|
@@ -2438,7 +2709,25 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2438
2709
|
* is born, not when a field is.
|
|
2439
2710
|
*/
|
|
2440
2711
|
if (override.detFromSchema) {
|
|
2441
|
-
|
|
2712
|
+
/**
|
|
2713
|
+
* One name or several, unioned by leaf path.
|
|
2714
|
+
*
|
|
2715
|
+
* An ILF's DETs are the fields the user recognises in the file, and an
|
|
2716
|
+
* application with one schema per template recognises all of them. A field
|
|
2717
|
+
* two templates share is one DET, so the union is over paths rather than a
|
|
2718
|
+
* sum of counts.
|
|
2719
|
+
*/
|
|
2720
|
+
const named = [override.detFromSchema].flat();
|
|
2721
|
+
const resolved = named.map((name) => schemas.get(name)).filter((s) => s !== void 0);
|
|
2722
|
+
const missing = named.filter((name) => !schemas.has(name));
|
|
2723
|
+
const union = new Set(resolved.flatMap((s) => s.leaves));
|
|
2724
|
+
const schema = resolved.length === 0 ? void 0 : {
|
|
2725
|
+
/** only what resolved: naming a schema that contributed nothing would mislead */
|
|
2726
|
+
name: resolved.map((s) => s.name).join(" + "),
|
|
2727
|
+
fields: union.size,
|
|
2728
|
+
leaves: [...union]
|
|
2729
|
+
};
|
|
2730
|
+
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
2731
|
if (schema) {
|
|
2443
2732
|
/**
|
|
2444
2733
|
* Replaces the opaque placeholder — the one the rationale marks — rather
|
|
@@ -2453,11 +2742,34 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2453
2742
|
det = Math.max(fn.det - Math.min(placeholders.length, 1), 0) + schema.fields;
|
|
2454
2743
|
by = `config:overrides.${fn.name} (from ${schema.name}: ${schema.fields} fields)`;
|
|
2455
2744
|
if (placeholders.length === 0) warnings.push(`override for "${fn.name}" names schema "${schema.name}", but this function has no opaque DET for it to stand in for: the ${schema.fields} fields were ADDED to the ${fn.det} already counted. Check the override is on the right function.`);
|
|
2456
|
-
else
|
|
2457
|
-
|
|
2745
|
+
else {
|
|
2746
|
+
/**
|
|
2747
|
+
* Only the placeholders nobody has answered are worth reporting.
|
|
2748
|
+
*
|
|
2749
|
+
* The message used to count every opaque DET of the function and say "names
|
|
2750
|
+
* one schema" whatever it was given. With four of five columns in
|
|
2751
|
+
* `opaqueReviewed` and a LIST of two schemas, it still fired, still said
|
|
2752
|
+
* "one schema", and still counted the four already answered — a warning
|
|
2753
|
+
* wrong on all three counts, about a configuration that was complete.
|
|
2754
|
+
*/
|
|
2755
|
+
const unanswered = placeholders.filter((source) => !reviewed.has(opaqueNameOf(source)) && !reviewed.has(shortOpaqueNameOf(source)));
|
|
2756
|
+
if (unanswered.length > 1) warnings.push(`override for "${fn.name}" names ${named.length === 1 ? "one schema" : `${named.length} schemas`} and the function has ${unanswered.length} unanswered opaque DETs (${unanswered.join(", ")}). One was replaced; the others still count 1 each — declare them or record them with \`opaqueReviewed\`.`);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2458
2759
|
}
|
|
2459
2760
|
const refs = override.refs ?? fn.refs;
|
|
2460
2761
|
const complexity = complexityOf(fn.type, refs, det, tables);
|
|
2762
|
+
/**
|
|
2763
|
+
* A review is recorded with NO fields, and the reporter's "declared by override"
|
|
2764
|
+
* share counts only entries that declared one.
|
|
2765
|
+
*
|
|
2766
|
+
* Dropping it entirely lost the `reason`, so an `opaqueReviewed`-only decision
|
|
2767
|
+
* appeared nowhere — not in `fp:explain`, not anywhere — which defeats the point
|
|
2768
|
+
* of requiring a reason. Counting it in the share was the opposite error: it read
|
|
2769
|
+
* as "1 function, 7 FP, 35% of the total declared by override" when no number had
|
|
2770
|
+
* been declared at all.
|
|
2771
|
+
*/
|
|
2772
|
+
const marked = markReviewed(fn.rationale.detSources, reviewed);
|
|
2461
2773
|
return {
|
|
2462
2774
|
...fn,
|
|
2463
2775
|
det,
|
|
@@ -2466,6 +2778,7 @@ function applyOverrides(functions, overrides, schemas, tables, weights, warnings
|
|
|
2466
2778
|
points: pointsOf(fn.type, complexity, weights),
|
|
2467
2779
|
rationale: {
|
|
2468
2780
|
...fn.rationale,
|
|
2781
|
+
detSources: marked,
|
|
2469
2782
|
overrides: [...fn.rationale.overrides ?? [], {
|
|
2470
2783
|
by,
|
|
2471
2784
|
reason: override.reason,
|
|
@@ -2494,7 +2807,16 @@ function usageOf(input) {
|
|
|
2494
2807
|
};
|
|
2495
2808
|
usage.set(store, {
|
|
2496
2809
|
used: true,
|
|
2497
|
-
|
|
2810
|
+
/**
|
|
2811
|
+
* Per STORE, not per transaction.
|
|
2812
|
+
*
|
|
2813
|
+
* `behavior.writes` decides EI against EO and says nothing about which of
|
|
2814
|
+
* the tables was written. Read as "this store is maintained", a reference
|
|
2815
|
+
* table merely READ by a route that writes something else became an ILF —
|
|
2816
|
+
* and on a production application that left exactly one EIF in the whole
|
|
2817
|
+
* count, which should have been the signal.
|
|
2818
|
+
*/
|
|
2819
|
+
written: current.written || behavior.writtenStores.includes(store)
|
|
2498
2820
|
});
|
|
2499
2821
|
}
|
|
2500
2822
|
}
|
|
@@ -2601,7 +2923,7 @@ function describeSource(root, config) {
|
|
|
2601
2923
|
]),
|
|
2602
2924
|
dirty: status === void 0 ? void 0 : status.length > 0,
|
|
2603
2925
|
countedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2604
|
-
config: config === null ? null :
|
|
2926
|
+
config: config === null ? null : relativeTo(root, config)
|
|
2605
2927
|
};
|
|
2606
2928
|
}
|
|
2607
2929
|
function appName(root) {
|
|
@@ -2635,46 +2957,87 @@ async function analyze(root, options = {}) {
|
|
|
2635
2957
|
const behaviors = new Map(entryPoints.filter((entry) => entry.handler).map((entry) => [entry.id, analyzer.analyze(entry.handler)]));
|
|
2636
2958
|
const resolved = [...behaviors.values()].filter((behavior) => behavior.unresolved.length === 0).length;
|
|
2637
2959
|
const unresolvedCalls = storeProblems.length + routeProblems.length + [...behaviors.values()].reduce((total, behavior) => total + behavior.unresolved.length, 0);
|
|
2960
|
+
/**
|
|
2961
|
+
* Every path that LEAVES is relative to the application root.
|
|
2962
|
+
*
|
|
2963
|
+
* `CountSource.app` is documented as never being the absolute path, because that
|
|
2964
|
+
* says where the machine keeps its files and travels with every artefact sent
|
|
2965
|
+
* anywhere. The rule was stated on one field and applied to one field: the
|
|
2966
|
+
* inventory carried 2036 absolute paths across ten of them, and a count carried
|
|
2967
|
+
* 858 in its traces alone.
|
|
2968
|
+
*
|
|
2969
|
+
* Absolute is right INTERNALLY — it is what ts-morph resolves and what the call
|
|
2970
|
+
* graph keys its caches on — so the conversion happens here, at the boundary, and
|
|
2971
|
+
* the data-store `id` is relativised only after the ancestor filter has used it.
|
|
2972
|
+
*/
|
|
2973
|
+
const source = describeSource(root, options.configFile ?? null);
|
|
2974
|
+
const emit = (value) => relativeTo(app.root, value);
|
|
2975
|
+
const emitProvenance = (p) => ({
|
|
2976
|
+
...p,
|
|
2977
|
+
file: emit(p.file)
|
|
2978
|
+
});
|
|
2638
2979
|
const inventory = {
|
|
2639
2980
|
version: 1,
|
|
2640
2981
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2641
|
-
app: app
|
|
2982
|
+
app: source.app,
|
|
2642
2983
|
framework: {
|
|
2643
2984
|
core: app.framework.core,
|
|
2644
2985
|
lucid: app.framework.lucid,
|
|
2645
2986
|
orm: app.framework.orm
|
|
2646
2987
|
},
|
|
2647
|
-
dataStores: stores
|
|
2648
|
-
|
|
2988
|
+
dataStores: stores.map((store) => ({
|
|
2989
|
+
...store,
|
|
2990
|
+
id: emit(store.id),
|
|
2991
|
+
provenance: emitProvenance(store.provenance),
|
|
2992
|
+
attributes: store.attributes.map((a) => ({
|
|
2993
|
+
...a,
|
|
2994
|
+
provenance: emitProvenance(a.provenance)
|
|
2995
|
+
}))
|
|
2996
|
+
})),
|
|
2997
|
+
entryPoints: entryPoints.map((entry) => ({
|
|
2998
|
+
...entry,
|
|
2999
|
+
provenance: emitProvenance(entry.provenance),
|
|
3000
|
+
handler: entry.handler ? {
|
|
3001
|
+
...entry.handler,
|
|
3002
|
+
file: emit(entry.handler.file)
|
|
3003
|
+
} : null
|
|
3004
|
+
})),
|
|
2649
3005
|
behaviors: [...behaviors.entries()].map(([entryPointId, behavior]) => ({
|
|
2650
3006
|
entryPointId,
|
|
2651
3007
|
writes: behavior.writes,
|
|
2652
3008
|
touches: behavior.touches,
|
|
3009
|
+
writtenStores: behavior.writtenStores,
|
|
2653
3010
|
inputFields: behavior.inputFields.map((name) => ({
|
|
2654
3011
|
name,
|
|
2655
3012
|
provenance: {
|
|
2656
|
-
file: app.root,
|
|
3013
|
+
file: emit(app.root),
|
|
2657
3014
|
by: "validator"
|
|
2658
3015
|
}
|
|
2659
3016
|
})),
|
|
2660
3017
|
opaqueInputFields: behavior.opaqueInputFields.map((name) => ({
|
|
2661
3018
|
name,
|
|
2662
3019
|
provenance: {
|
|
2663
|
-
file: app.root,
|
|
3020
|
+
file: emit(app.root),
|
|
2664
3021
|
by: "validator"
|
|
2665
3022
|
}
|
|
2666
3023
|
})),
|
|
2667
3024
|
requestFields: behavior.requestFields.map((name) => ({
|
|
2668
3025
|
name,
|
|
2669
3026
|
provenance: {
|
|
2670
|
-
file: app.root,
|
|
3027
|
+
file: emit(app.root),
|
|
2671
3028
|
by: "request"
|
|
2672
3029
|
}
|
|
2673
3030
|
})),
|
|
2674
3031
|
opaqueRequest: behavior.opaqueRequest,
|
|
2675
3032
|
outputFields: [],
|
|
2676
|
-
trace: behavior.trace
|
|
2677
|
-
|
|
3033
|
+
trace: behavior.trace.map((step) => ({
|
|
3034
|
+
...step,
|
|
3035
|
+
file: emit(step.file)
|
|
3036
|
+
})),
|
|
3037
|
+
unresolved: behavior.unresolved.map((call) => ({
|
|
3038
|
+
...call,
|
|
3039
|
+
file: emit(call.file)
|
|
3040
|
+
}))
|
|
2678
3041
|
})),
|
|
2679
3042
|
coverage: {
|
|
2680
3043
|
entryPointsTotal: entryPoints.length,
|
|
@@ -2696,7 +3059,7 @@ async function analyze(root, options = {}) {
|
|
|
2696
3059
|
jsonSchemas,
|
|
2697
3060
|
writtenAnywhere: analyzer.writtenAnywhere()
|
|
2698
3061
|
}, options),
|
|
2699
|
-
source
|
|
3062
|
+
source
|
|
2700
3063
|
}
|
|
2701
3064
|
};
|
|
2702
3065
|
}
|