@afokapu/atdd-bun 0.10.68 → 0.10.69
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/detectors/atdd_traceability_closure/detect.mjs +5 -1
- package/detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs +36 -5
- package/detectors/bun_ts_metrics_detector/checks/ts_dead_code.mjs +11 -1
- package/integrity.json +5 -5
- package/lib/journey-chain.mjs +29 -1
- package/package.json +1 -1
|
@@ -62,7 +62,11 @@ for (const root of roots) {
|
|
|
62
62
|
// EVERY binding header is judged, not only the first: a valid Acceptance: must not carry an unresolved Train:.
|
|
63
63
|
const headers = [...head.matchAll(/^[ \t]*\/\/[ \t]*(Acceptance|WMBT|Train):[ \t]*(\S*)/gm)].map((m) => ({ kind: m[1], id: m[2], raw: m[0].trim(), line: lineAt(head, m.index) }));
|
|
64
64
|
const bindings = headers.filter((h) => h.id.startsWith(PREFIX[h.kind]));
|
|
65
|
-
|
|
65
|
+
// A test identity may be reused by equivalent smoke harnesses. Preserve every
|
|
66
|
+
// independently declared binding: replacing the earlier record made a valid
|
|
67
|
+
// acceptance disappear solely because a later file had the same test URN.
|
|
68
|
+
const prior = tests.get(urn[1]);
|
|
69
|
+
tests.set(urn[1], { path, bindings: [...(prior?.bindings ?? []), ...bindings] });
|
|
66
70
|
if (!headers.length) add("traceability.test.binding-resolves", path, lineOf(head, urn[0]), "test has a URN but no Acceptance:, WMBT:, or Train: binding", urn[0]);
|
|
67
71
|
for (const h of headers) if (!bindings.includes(h)) add("traceability.test.binding-resolves", path, h.line, `${h.kind}: ${h.id || "<empty>"} is not a ${PREFIX[h.kind]} identity`, h.raw);
|
|
68
72
|
return;
|
|
@@ -47,25 +47,56 @@ function journeyRunnerLine(text) {
|
|
|
47
47
|
return match ? lineOfIndex(text, match.index) : 1;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Declaration helpers that verifiably read AND parse the file named by their own parameter.
|
|
51
|
+
function declarationHelpers(masked) {
|
|
52
|
+
const names = [];
|
|
53
|
+
const helper = /\b(?:async\s+)?function\s+((?=[A-Za-z_$\w]*(?:declaration|journey))[A-Za-z_$][\w$]*)\s*\(\s*([A-Za-z_$][\w$]*)?[^)]*\)\s*(?::[^\{]+)?\{([\s\S]*?)\n\}/gi;
|
|
54
|
+
for (const match of masked.matchAll(helper)) {
|
|
55
|
+
const [, name, param, body] = match;
|
|
56
|
+
if (!param || !/\b(?:Bun\.YAML\.parse|JSON\.parse)\s*\(/.test(body)) continue;
|
|
57
|
+
// The read argument must reference the helper's own parameter, not a fixed path.
|
|
58
|
+
const paramName = param.replace(/[$]/g, "\\$");
|
|
59
|
+
if (!new RegExp(`\\b(?:Bun\\.file|readFile(?:Sync)?)\\s*\\(\\s*[^)]*\\b${paramName}\\b`).test(body)) continue;
|
|
60
|
+
// JourneyRunner must pass its own declared path to that helper.
|
|
61
|
+
const callee = name.replace(/[$]/g, "\\$");
|
|
62
|
+
if (new RegExp(`\\b${callee}\\s*\\(\\s*this\\s*\\.\\s*journeyYamlPath\\s*\\)`).test(masked)) names.push(name);
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
function loadsJourneyDeclaration(text) {
|
|
51
68
|
const masked = maskComments(text);
|
|
52
69
|
const readsJourneyPath =
|
|
53
70
|
/Bun\.file\s*\(\s*[^)]*(?:journey|_journeys)/i.test(masked) ||
|
|
54
71
|
/\breadFile(?:Sync)?\s*\(\s*[^,)]*(?:journey|_journeys)/i.test(masked);
|
|
55
72
|
const parsesDeclaration = /Bun\.YAML\.parse\s*\(|JSON\.parse\s*\(/.test(masked);
|
|
56
|
-
|
|
73
|
+
if (readsJourneyPath && parsesDeclaration) return true;
|
|
74
|
+
// Production runners may centralize parsing and validation in a declaration helper.
|
|
75
|
+
return declarationHelpers(masked).length > 0;
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
function consumesDeclaredTopology(text) {
|
|
60
79
|
const masked = maskComments(text);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
80
|
+
// The topology variable must come from the original parse or from the one verified helper
|
|
81
|
+
// called with this.journeyYamlPath; a topology from any other call is not the declaration.
|
|
82
|
+
const escape = (value) => value.replace(/[$]/g, "\\$");
|
|
83
|
+
const sources = ["Bun\\.YAML\\.parse\\s*\\(", ...declarationHelpers(masked).map(helper =>
|
|
84
|
+
`(?:await\\s+)?${escape(helper)}\\s*\\(\\s*this\\s*\\.\\s*journeyYamlPath\\s*\\)`)];
|
|
85
|
+
const declarations = [...masked.matchAll(new RegExp("\\b(?:const|let)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*(?:" + sources.join("|") + ")", "g"))];
|
|
86
|
+
return declarations.some(declaration => consumesTopologyFrom(masked, declaration[1]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function consumesTopologyFrom(masked, variable) {
|
|
90
|
+
const name = variable.replace(/[$]/g, "\\$");
|
|
64
91
|
const reads = (field) => new RegExp("\\b" + name + "\\s*\\.\\s*" + field + "\\s*\\.\\s*find\\s*\\(").test(masked);
|
|
65
92
|
// A runner must retain the interlocking result long enough to decide whether the
|
|
66
93
|
// declared route continues or terminates. Returning the first execute() directly
|
|
67
94
|
// is the characteristic skipped-continuation mutation.
|
|
68
|
-
const
|
|
95
|
+
const execution = "await\\s+(?:new\\s+InterlockingRunner\\s*\\([^)]*\\)|[A-Za-z_$][\\w$]*)\\s*\\.\\s*execute\\s*\\(";
|
|
96
|
+
// The result may also be declared first and assigned once inside a try that has a catch:
|
|
97
|
+
// `let run: T; try { run = await runner.execute(...) } catch { throw error }`.
|
|
98
|
+
const retainsResolution = new RegExp("\\b(?:const|let)\\s+[A-Za-z_$][\\w$]*\\s*=\\s*" + execution).test(masked) ||
|
|
99
|
+
new RegExp("\\blet\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=;]+)?;\\s*try\\s*\\{\\s*\\1\\s*=\\s*" + execution + "[\\s\\S]{0,400}?\\}\\s*catch\\s*(?:\\([^)]*\\))?\\s*\\{[^}]*\\b(?:throw|return)\\b").test(masked);
|
|
69
100
|
const readsEntrypoint = new RegExp("\\b" + name + "\\s*\\.\\s*entrypoint\\b").test(masked);
|
|
70
101
|
// Reading both lists is only discovery. A continuation must alter control flow to
|
|
71
102
|
// another declared interlocking and a terminal must decide the returned outcome;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// obligation, measured correctly, and only possible because the runtime is Bun.
|
|
13
13
|
import { walk, readRoots, readExcludes, readText, emit } from "../../../lib/scan.mjs";
|
|
14
14
|
import * as M from "../ts_metrics.mjs";
|
|
15
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
15
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
17
17
|
|
|
18
18
|
const RULE = "coder.bun.dead-code-reachability";
|
|
@@ -64,6 +64,16 @@ for (const root of readRoots()) {
|
|
|
64
64
|
if (!roots.size) continue;
|
|
65
65
|
|
|
66
66
|
const forward = M.reachableFrom(roots, graph);
|
|
67
|
+
// Tests are executable entrypoints, but only for test-support modules (e2e/, tests/ ...). A
|
|
68
|
+
// production module imported solely by a test is still dead production code.
|
|
69
|
+
const SUPPORT_SEGMENTS = new Set(["e2e", "test", "tests", "__tests__", "spec", "fixtures"]);
|
|
70
|
+
const isSupport = (f) => relative(root, f).split(/[\\/]/).slice(0, -1).some(segment => SUPPORT_SEGMENTS.has(segment));
|
|
71
|
+
const queue = files.filter(M.isTestFile);
|
|
72
|
+
while (queue.length) {
|
|
73
|
+
for (const next of graph.get(queue.pop()) || []) {
|
|
74
|
+
if (isSupport(next) && !forward.has(next)) { forward.add(next); queue.push(next); }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
67
77
|
const backward = M.reachableFrom(roots, M.reverseGraph(graph));
|
|
68
78
|
|
|
69
79
|
for (const f of files) {
|
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.69",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"detectors/atdd_topology/fixtures/dirty/src/wagons/orders/features/place-order/domain/order.ts": "6fdaad52671ef25b47d93f9b4e97e12c62eb78f5b4cde727435c3ac135a1c0e5",
|
|
192
192
|
"detectors/atdd_topology/fixtures/dirty/tests/wagons/orders/features/place-order/unit/order.test.ts": "f117e6c8b034700e1f00889bea48cb1528f0003d699cfd3e279cd9164b264a38",
|
|
193
193
|
"detectors/atdd_traceability_closure/atdd.implementation.yaml": "82e56cc85539476e42fc8d58e718bc998f1eff6c325a0cd240c0284625834d68",
|
|
194
|
-
"detectors/atdd_traceability_closure/detect.mjs": "
|
|
194
|
+
"detectors/atdd_traceability_closure/detect.mjs": "17582d256ee4b1555bd62ba4decfe2ff15b664400a243ff28b0e87604d3aa05d",
|
|
195
195
|
"detectors/atdd_traceability_closure/fixtures/clean/plan/orders/E001.yaml": "0ddad928dde10c2ab690558259a6975125af540a1f30eca9aa0c615bf0ad69fb",
|
|
196
196
|
"detectors/atdd_traceability_closure/fixtures/clean/src/order.ts": "efe6db7e3f723c7b6096abdf58dec9c88b27ec8978d434a10135b3e77bfce321",
|
|
197
197
|
"detectors/atdd_traceability_closure/fixtures/clean/test/order.test.ts": "bee451198d472749616f85c1f1e97c0a06ad28255b9a6a2cc8701bd2e8d56562",
|
|
@@ -490,7 +490,7 @@
|
|
|
490
490
|
"detectors/bun_interlocking_infrastructure/checks/interlocking_resolution_model.mjs": "e761495b19eed0af09fcb4db34126d0e2398e91f086e04f5ef84d0b5c1c3ec16",
|
|
491
491
|
"detectors/bun_interlocking_infrastructure/checks/interlocking_runner_exists.mjs": "6b63d3f26a51f2dd27847edd27c4d9c9f15c1f02747dba611cebccfc76f13169",
|
|
492
492
|
"detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs": "f3870fb3bacee46a6c3e10238284ef4d359182030d40d4f33b0f0ad465f7a284",
|
|
493
|
-
"detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs": "
|
|
493
|
+
"detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs": "6284a8532722e5a6bff7e5184e24c7f273c1e63f1331204502e0e3704db5c86b",
|
|
494
494
|
"detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs": "66c09cdc17b099a55fa0ca894bd85a44706742bdd29caa5347f10e6487b4e724",
|
|
495
495
|
"detectors/bun_interlocking_infrastructure/checks/wagon_contract_honoured.mjs": "79b5f824e7510f50e4925a98c01ef263290025748f64a27a11555f608d8001dc",
|
|
496
496
|
"detectors/bun_interlocking_infrastructure/detect.mjs": "6494579ec7b9d4680099401346337d73f4a52368ffb6b9d3b0fbca5dce2cde15",
|
|
@@ -717,7 +717,7 @@
|
|
|
717
717
|
"detectors/bun_ts_metrics_detector/checks/ts_complexity_cyclomatic.mjs": "56640b3a0f565c45a73b7c174adb1d6b4b6ac24d286a907bc1426f013cd6039e",
|
|
718
718
|
"detectors/bun_ts_metrics_detector/checks/ts_complexity_length.mjs": "e49fe93dbf2c52a91fe97b1e0b810d60f7d34d43581dfb96d4827640e6f5e3fd",
|
|
719
719
|
"detectors/bun_ts_metrics_detector/checks/ts_complexity_nesting.mjs": "8d8f61ea1748f33b037beb3f3fe5c84acbce395575e24fb1d01432813762e4d1",
|
|
720
|
-
"detectors/bun_ts_metrics_detector/checks/ts_dead_code.mjs": "
|
|
720
|
+
"detectors/bun_ts_metrics_detector/checks/ts_dead_code.mjs": "8f2205274f5056993a888ea4141be4d0b64c287e4641d0a464d178530b5297d4",
|
|
721
721
|
"detectors/bun_ts_metrics_detector/checks/ts_duplication.mjs": "12b5087c34741ce3fde573059d1afbaafc80ce1bfe8a235780113fd558dbbc7d",
|
|
722
722
|
"detectors/bun_ts_metrics_detector/checks/ts_quality_comments.mjs": "872d71d2165eba4cb39c975b09365cf67a64f1db5c55c0df9e86ff411e02ea31",
|
|
723
723
|
"detectors/bun_ts_metrics_detector/checks/ts_quality_mi.mjs": "69765d88441e98f4f38b7da0a416b7dab8b7f0072e562ea9d40d399802452067",
|
|
@@ -1034,7 +1034,7 @@
|
|
|
1034
1034
|
"lib/frontend.mjs": "d6d9b29c3d7f2d162cda03bb34b20a3224a82887b6be3db7cab375510913c9a3",
|
|
1035
1035
|
"lib/imports.mjs": "3db4979269488fa26f6e7aefded6ece9c646be6587a10e249821a5a208109962",
|
|
1036
1036
|
"lib/interlocking.mjs": "d0803d080ac7d7477c51be70112df1e85213d339fd3c6b71f75f26145fa8accd",
|
|
1037
|
-
"lib/journey-chain.mjs": "
|
|
1037
|
+
"lib/journey-chain.mjs": "aaa0e6acdd73f1a8cfeea9a523a86c6d3133c020c37f97e7ec61db00f957ec6a",
|
|
1038
1038
|
"lib/scan.mjs": "02b0fa864cab24157a8f27b50a87a2bb66a875f90cf6632c753c4627d61456ef",
|
|
1039
1039
|
"planner-nodes/ENFORCEMENT_SCOPE.yaml": "d8ec7039c219857f3d77af5d4d84612b533b69c1a5fcef935fbf46470adafed4",
|
|
1040
1040
|
"planner-nodes/nodes/planner.acceptance.abstract-fields-required.convention.yaml": "bd565693f0ae861f0561ad2f7a91a8c55f9a209c3acd59c5750cfcffd96a85d8",
|
package/lib/journey-chain.mjs
CHANGED
|
@@ -430,12 +430,40 @@ function liveReturns(fn) {
|
|
|
430
430
|
// arms of a conditional, as a member/spread of it, an object property or array element, an
|
|
431
431
|
// `Object.freeze` argument, or a `const` bound to it. Logical, comma, arithmetic, unary (`void`, `!`)
|
|
432
432
|
// and calls to anything else replace or may skip the value, so they never carry it.
|
|
433
|
+
// A runner result may be declared before a try and assigned exactly once in the
|
|
434
|
+
// try body. The catch must itself exit (throw or return); otherwise a swallowed error would
|
|
435
|
+
// let the later return observe an unassigned value. Do not generalize this to ordinary
|
|
436
|
+
// `let` assignments: conditional or multiply-written values are not a proof chain.
|
|
437
|
+
function caughtSingleAssignment(ctx, identifier) {
|
|
438
|
+
const declaration = declarationOf(ctx, identifier);
|
|
439
|
+
if (!declaration || !ts.isVariableDeclaration(declaration) || declaration.initializer ||
|
|
440
|
+
!ts.isVariableDeclarationList(declaration.parent) || !(declaration.parent.flags & ts.NodeFlags.Let)) return null;
|
|
441
|
+
let fn = declaration.parent;
|
|
442
|
+
while (fn && !ts.isFunctionLike(fn)) fn = fn.parent;
|
|
443
|
+
if (!fn || !fn.body) return null;
|
|
444
|
+
const writes = [];
|
|
445
|
+
walk(fn.body, (node) => {
|
|
446
|
+
if (!ts.isBinaryExpression(node) || node.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
|
447
|
+
!ts.isIdentifier(unwrap(node.left)) || symbolOf(ctx, unwrap(node.left)) !== symbolOf(ctx, identifier) || dead(node, fn.body)) return;
|
|
448
|
+
let inCaughtTry = false;
|
|
449
|
+
for (let parent = node.parent; parent && parent !== fn.body; parent = parent.parent) {
|
|
450
|
+
if (ts.isTryStatement(parent) && parent.tryBlock && node.pos >= parent.tryBlock.pos && node.end <= parent.tryBlock.end && parent.catchClause &&
|
|
451
|
+
parent.catchClause.block.statements.some(statement => ts.isThrowStatement(statement) || ts.isReturnStatement(statement))) {
|
|
452
|
+
inCaughtTry = true;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (inCaughtTry) writes.push(node.right);
|
|
457
|
+
}, false);
|
|
458
|
+
return writes.length === 1 ? writes[0] : null;
|
|
459
|
+
}
|
|
460
|
+
|
|
433
461
|
function carries(ctx, expression, matches, seen = new Set()) {
|
|
434
462
|
const node = unwrap(expression);
|
|
435
463
|
if (!node) return false;
|
|
436
464
|
if (matches(node)) return true;
|
|
437
465
|
if (ts.isIdentifier(node)) {
|
|
438
|
-
const initializer = constInitializer(ctx, node);
|
|
466
|
+
const initializer = constInitializer(ctx, node) ?? caughtSingleAssignment(ctx, node);
|
|
439
467
|
// `seen` guards cycles along this path only; sibling branches may resolve the same const.
|
|
440
468
|
if (!initializer || seen.has(initializer)) return false;
|
|
441
469
|
seen.add(initializer);
|