@afokapu/atdd-bun 0.10.79 → 0.10.81
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/conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml +1 -0
- package/detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs +4 -1
- package/detectors/bun_ts_metrics_detector/ts_metrics.mjs +32 -1
- package/integrity.json +5 -5
- package/lib/journey-chain.mjs +75 -0
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ content:
|
|
|
56
56
|
- Test, spec, and `.d.ts` files are excluded, as are `index.ts` barrels and `__tests__` directories.
|
|
57
57
|
- Trivial lines (bare braces, closing punctuation) are not counted toward the 7.
|
|
58
58
|
- Import and re-export declarations are not counted, because two files importing the same names from one shared module match because the logic was shared, which is the fix this rule asks for.
|
|
59
|
+
- Property signatures inside a `type X = {` or `interface X {` body (optional `readonly`, a name, optional `?`, `:` a type, ending `;` or `,`), including the members of nested object types, are not counted, because two unrelated records with the same number of same-typed fields match once identifiers are folded and neither coupling them nor a cosmetic edit is a fix. Executable statements, object literals, class fields, enum bodies, and method bodies still count.
|
|
59
60
|
- Files whose layer cannot be determined are compared only against each other.
|
|
60
61
|
metadata:
|
|
61
62
|
aliases:
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
mk,
|
|
30
30
|
writeReport,
|
|
31
31
|
} from "../_shared/interlocking.mjs";
|
|
32
|
+
import { assertedJourneyExecution } from "../../../lib/journey-chain.mjs";
|
|
32
33
|
|
|
33
34
|
const RULE = "tester.bun.interlocking-production-runner-used";
|
|
34
35
|
const roots = parseJsonEnv("ATDD_SCAN_ROOTS", []);
|
|
@@ -72,7 +73,9 @@ for (const scanRoot of roots) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
// A journey test drives the runners through the production JourneyRunner: its asserted result counts
|
|
77
|
+
// when the journey-mediated chain is proven on the AST (lib/journey-chain.mjs), as for the Station Master.
|
|
78
|
+
const semantic = hasProductionExecutionProof(text, file, croot) || assertedJourneyExecution(raw, file, croot) ||
|
|
76
79
|
records.some((rec) => rec.actions.some((action) => stationMasterExecutionProof(text, action))) ||
|
|
77
80
|
(drivesStationMaster(text) && records.some((rec) => rec.actions.some((action) =>
|
|
78
81
|
stationModuleExecutionProof(text, station.text, action, station.file, croot),
|
|
@@ -234,19 +234,50 @@ export const isTrivial = (n) => TRIVIAL.has(n);
|
|
|
234
234
|
const DECLARATION = /^(import\b|export (type )?(\{|\*))/;
|
|
235
235
|
const DECLARATION_END = /\bfrom "S"|^import "S"|\}/;
|
|
236
236
|
|
|
237
|
+
// Property signatures inside a `type X = {` / `interface X {` body are not code either: once
|
|
238
|
+
// identifiers are folded, two unrelated records with seven `readonly id: string;` members match
|
|
239
|
+
// line for line, and neither coupling the records nor a cosmetic edit is a fix. A member line
|
|
240
|
+
// (optional `readonly`, a name, optional `?`, `:` a type, ending `;` or `,`), a member that opens a
|
|
241
|
+
// nested object type, and that nested type's closing line are left out of the windows. The body
|
|
242
|
+
// is tracked by brace depth; anything else inside it (a method signature, say) still counts.
|
|
243
|
+
const TYPE_BODY = /^(export )?(declare )?(type ID(<[^>]*>)? = \{|interface ID(<.*>)?( extends [^{]*)? \{)$/;
|
|
244
|
+
const MEMBER = /^(readonly )?(ID|"S")\??: (.*[;,]|\{)$|^\}[;,]?$/;
|
|
245
|
+
const braceDelta = (n) => (n.match(/\{/g) ?? []).length - (n.match(/\}/g) ?? []).length;
|
|
246
|
+
// normalizeLine strips a block comment only when it closes on the same line, so a multi-line
|
|
247
|
+
// comment's text (say a JSDoc ` * shape like { a`) would reach braceDelta and keep the body open
|
|
248
|
+
// for the rest of the file. The body is tracked on the code part of each line only; whether the
|
|
249
|
+
// line counts toward a window is unchanged.
|
|
250
|
+
function codePart(n, inComment) {
|
|
251
|
+
let code = n;
|
|
252
|
+
if (inComment) {
|
|
253
|
+
const close = code.indexOf("*/");
|
|
254
|
+
if (close < 0) return { code: "", inComment: true };
|
|
255
|
+
code = code.slice(close + 2).trim();
|
|
256
|
+
}
|
|
257
|
+
const open = code.indexOf("/*");
|
|
258
|
+
return open < 0 ? { code, inComment: false } : { code: code.slice(0, open).trim(), inComment: true };
|
|
259
|
+
}
|
|
260
|
+
|
|
237
261
|
// A 16-hex-char digest of each `minLines` window of non-trivial normalized lines —
|
|
238
262
|
// the same fingerprint shape the Python detector hashes with sha256[:16].
|
|
239
263
|
export function fragments(source, minLines, hasher) {
|
|
240
264
|
const nonTrivial = [];
|
|
241
265
|
let inDeclaration = false;
|
|
266
|
+
let typeDepth = 0;
|
|
267
|
+
let inComment = false;
|
|
242
268
|
source.split("\n").forEach((line, i) => {
|
|
243
269
|
const n = normalizeLine(line);
|
|
270
|
+
const part = codePart(n, inComment);
|
|
271
|
+
inComment = part.inComment;
|
|
244
272
|
if (!inDeclaration && DECLARATION.test(n)) inDeclaration = true;
|
|
245
273
|
if (inDeclaration) {
|
|
246
274
|
if (DECLARATION_END.test(n)) inDeclaration = false;
|
|
247
275
|
return;
|
|
248
276
|
}
|
|
249
|
-
|
|
277
|
+
const member = typeDepth > 0 && MEMBER.test(part.code);
|
|
278
|
+
if (typeDepth > 0) typeDepth = Math.max(0, typeDepth + braceDelta(part.code));
|
|
279
|
+
else if (TYPE_BODY.test(part.code)) typeDepth = 1;
|
|
280
|
+
if (!member && !isTrivial(n)) nonTrivial.push([i + 1, n]);
|
|
250
281
|
});
|
|
251
282
|
if (nonTrivial.length < minLines) return [];
|
|
252
283
|
const out = [];
|
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.81",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"conventions/coder.bun/coder.bun.dto-mapper.convention.yaml": "c524b5f37db6ae4f7558fc3ec672aa1c5b764738a0f25a20062d736fed6bbcb3",
|
|
60
60
|
"conventions/coder.bun/coder.bun.dto-placement.convention.yaml": "9525255f9e037ca0d939de20e70235fe348dcebf2136536055bc21433d7e9fb5",
|
|
61
61
|
"conventions/coder.bun/coder.bun.dto-purity.convention.yaml": "4743199fb242a25f896425916e3873c2e8cf62f4285437f62b0f8f8139d6a58d",
|
|
62
|
-
"conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml": "
|
|
62
|
+
"conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml": "023310241a28e6a7aad5cf020bd1b9d4e05a887ddd92dbcd4c13b8a45e26ae6e",
|
|
63
63
|
"conventions/coder.bun/coder.bun.error-response-bare-string.convention.yaml": "c2c0f859f6dd12702edf225c4148849e72b6c25e6d7cd28edd5d644ba277a23c",
|
|
64
64
|
"conventions/coder.bun/coder.bun.error-response-code-format.convention.yaml": "3048fed00b92c2559f7c52f73cfcce07b583eb47b12d04f0851e237ae3646567",
|
|
65
65
|
"conventions/coder.bun/coder.bun.green-header-order.convention.yaml": "2a42d67bedeec1de76fa1de844351983c5ba4bc5b61d5a5047298f6804664f67",
|
|
@@ -388,7 +388,7 @@
|
|
|
388
388
|
"detectors/bun_interlocking_coverage/_shared/interlocking.mjs": "8a90490daaaa6eecf4c971fb569da38156b45969d48755dbcf355175dde0b079",
|
|
389
389
|
"detectors/bun_interlocking_coverage/atdd.implementation.yaml": "f160517a8138ba3ca05e3d130e6fbfb796e643376f25743fdef4fc210196525b",
|
|
390
390
|
"detectors/bun_interlocking_coverage/checks/_map.json": "0a218ea319cc6258af7bd721c2154cb9561fe67e80d80ef32d08fc77bc9403c3",
|
|
391
|
-
"detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs": "
|
|
391
|
+
"detectors/bun_interlocking_coverage/checks/interlocking_production_runner.mjs": "97224d46f0c4c3259f15e41f90bb7f9f5a4e651d4facd41f1e037cac919be500",
|
|
392
392
|
"detectors/bun_interlocking_coverage/checks/interlocking_route_coverage.mjs": "a2cff94a6bf9e11fe8e370332551742f9c7db317dcd57a88d08dc6c74f868f49",
|
|
393
393
|
"detectors/bun_interlocking_coverage/checks/interlocking_smoke_coverage.mjs": "28e016e1a4cd394291640679876b16c7d6a725530c7fbfa4bd1da8a354ecff86",
|
|
394
394
|
"detectors/bun_interlocking_coverage/checks/interlocking_trace_binds.mjs": "1cc3a11d50a075f3211a6d9cbf27aafcbf7e77b6a855ab040f765ec89dfe0eef",
|
|
@@ -741,7 +741,7 @@
|
|
|
741
741
|
"detectors/bun_ts_metrics_detector/fixtures/dirty/long.ts": "b87149b1b550a00bc2de2b2b21a4d9925572c002221941fae8135e8fe0c27cd1",
|
|
742
742
|
"detectors/bun_ts_metrics_detector/fixtures/dirty/orphan.ts": "0f7acd0dffe90902a0a43cc45771b8a473d079efc9693112b3b85645c191c23d",
|
|
743
743
|
"detectors/bun_ts_metrics_detector/fixtures/dirty/typed.ts": "7c27ee1a674af7d5b2cce3a65bfec2111602d84a75817fd60f737c7d63fc9c83",
|
|
744
|
-
"detectors/bun_ts_metrics_detector/ts_metrics.mjs": "
|
|
744
|
+
"detectors/bun_ts_metrics_detector/ts_metrics.mjs": "7ccfd393b38b0d0f45ab13bffaefdeeef782818af5194d8ff3d6fce5ae34ff44",
|
|
745
745
|
"detectors/htmx_e2e_detector/atdd.implementation.yaml": "454bb123039d57c992d27d9274e20fa3452ac41a237eb55471be62a4bcfebf06",
|
|
746
746
|
"detectors/htmx_e2e_detector/checks/_e2e.mjs": "efec6c8e3dc8b95bf3acd826397ecdedac64c06d367791a4017a272bc0660896",
|
|
747
747
|
"detectors/htmx_e2e_detector/checks/_map.json": "94dec08ff4764cd6a3dba139f12a7c984292e95a9354b0cdaa0ce9d4180e2c89",
|
|
@@ -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": "bdafc9cc3da4ade644aebacc6e1015e29285b764465588d764b7c581bade9025",
|
|
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
|
@@ -647,6 +647,81 @@ export function provenJourneyAction(stationText, stationFile, croot, name) {
|
|
|
647
647
|
everyModule(modules, "JourneyRunner", journeyExecutes(croot));
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
+
// The tester assertion vocabulary (shared tester layer `assertsExpression`), positive and under `.not`.
|
|
651
|
+
const MATCHERS = new Set(["toBe", "toBeDefined", "toEqual", "toStrictEqual", "toContain", "toContainEqual", "toMatchObject", "toMatch", "toBeTruthy"]);
|
|
652
|
+
const NEGATED_MATCHERS = new Set([...MATCHERS].filter((name) => name !== "toBeTruthy"));
|
|
653
|
+
|
|
654
|
+
// The journey declaration at `path` (relative to the consumer root, where its tests run) declares
|
|
655
|
+
// `action` among its entrypoint actions.
|
|
656
|
+
function declaresAction(croot, path, action) {
|
|
657
|
+
const file = resolve(croot, path);
|
|
658
|
+
if (!file.startsWith(resolve(croot) + sep) || !existsSync(file) || !statSync(file).isFile() || !globalThis.Bun?.YAML) return false;
|
|
659
|
+
try {
|
|
660
|
+
const actions = Bun.YAML.parse(readFileSync(file, "utf8"))?.entrypoint?.actions;
|
|
661
|
+
return Array.isArray(actions) && actions.includes(action);
|
|
662
|
+
} catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// A test module asserts a production JourneyRunner execution result, proven like provenJourneyAction:
|
|
668
|
+
// - a live `const` bound to `await <receiver>.execute("<action>", ...)`, the receiver being
|
|
669
|
+
// `new JourneyRunner("<journey declaration>")` or a const bound to one;
|
|
670
|
+
// - JourneyRunner binds to a local production import, and the declaration it is constructed from
|
|
671
|
+
// exists under the consumer root and declares that action;
|
|
672
|
+
// - the JourneyRunner module reaches InterlockingRunner -> TrainRunner (journeyExecutes);
|
|
673
|
+
// - a live `expect(<result or a member chain of it>).<matcher>(...)` whose argument binds to that
|
|
674
|
+
// const by checker symbol, not by spelling.
|
|
675
|
+
// The override contract covers the test module and every local module it reaches.
|
|
676
|
+
export function assertedJourneyExecution(testText, testFile, croot) {
|
|
677
|
+
if (!testText || !testFile || !croot) return false;
|
|
678
|
+
const ctx = analyze(testFile, testText);
|
|
679
|
+
if (tampered(ctx) || [...localImportClosure(testFile, croot)].some((module) => tampered(analyze(module)))) return false;
|
|
680
|
+
const results = new Set();
|
|
681
|
+
walk(ctx.source, (node) => {
|
|
682
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer ||
|
|
683
|
+
!(ts.isVariableDeclarationList(node.parent) && node.parent.flags & ts.NodeFlags.Const)) return;
|
|
684
|
+
const awaited = ts.isAwaitExpression(node.initializer) ? unwrap(node.initializer) : null;
|
|
685
|
+
const action = awaited && ts.isCallExpression(awaited) && awaited.arguments[0];
|
|
686
|
+
if (!action || !ts.isStringLiteralLike(action) || dead(node, ctx.source) || insideCatch(node, ctx.source)) return;
|
|
687
|
+
const fromDeclaration = (construction) => {
|
|
688
|
+
const path = construction.arguments?.[0];
|
|
689
|
+
return Boolean(path && ts.isStringLiteralLike(path) && declaresAction(croot, path.text, action.text));
|
|
690
|
+
};
|
|
691
|
+
const modules = new Set();
|
|
692
|
+
if (executesImported(ctx, "JourneyRunner", croot, modules, fromDeclaration)(awaited) &&
|
|
693
|
+
everyModule(modules, "JourneyRunner", journeyExecutes(croot))) results.add(symbolOf(ctx, node.name));
|
|
694
|
+
});
|
|
695
|
+
if (!results.size) return false;
|
|
696
|
+
const boundToResult = (node) => {
|
|
697
|
+
node = unwrap(node);
|
|
698
|
+
while (node && ts.isPropertyAccessExpression(node)) node = unwrap(node.expression);
|
|
699
|
+
return Boolean(node) && ts.isIdentifier(node) && results.has(symbolOf(ctx, node));
|
|
700
|
+
};
|
|
701
|
+
const isExpect = (node) => {
|
|
702
|
+
if (!ts.isIdentifier(node) || node.text !== "expect") return false;
|
|
703
|
+
const declaration = declarationOf(ctx, node);
|
|
704
|
+
if (!declaration) return true;
|
|
705
|
+
const statement = ts.isImportSpecifier(declaration) ? declaration.parent.parent.parent : null;
|
|
706
|
+
return Boolean(statement && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "bun:test" &&
|
|
707
|
+
(declaration.propertyName ?? declaration.name).text === "expect");
|
|
708
|
+
};
|
|
709
|
+
let asserted = false;
|
|
710
|
+
walk(ctx.source, (node) => {
|
|
711
|
+
if (asserted) return false;
|
|
712
|
+
if (!ts.isCallExpression(node) || !isExpect(node.expression) || !boundToResult(node.arguments[0]) || dead(node, ctx.source)) return;
|
|
713
|
+
let access = node.parent, matchers = MATCHERS;
|
|
714
|
+
if (!ts.isPropertyAccessExpression(access) || access.expression !== node) return;
|
|
715
|
+
if (access.name.text === "not") {
|
|
716
|
+
access = access.parent;
|
|
717
|
+
matchers = NEGATED_MATCHERS;
|
|
718
|
+
}
|
|
719
|
+
asserted = ts.isPropertyAccessExpression(access) && matchers.has(access.name.text) &&
|
|
720
|
+
ts.isCallExpression(access.parent) && access.parent.expression === access;
|
|
721
|
+
});
|
|
722
|
+
return asserted;
|
|
723
|
+
}
|
|
724
|
+
|
|
650
725
|
// Names of every exported Station Master action proven by provenJourneyAction.
|
|
651
726
|
export function provenJourneyActions(stationText, stationFile, croot) {
|
|
652
727
|
if (!stationText || !stationFile || !croot) return [];
|