@afokapu/atdd-bun 0.10.80 → 0.10.82

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.
@@ -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
- const semantic = hasProductionExecutionProof(text, file, croot) ||
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),
@@ -70,24 +70,36 @@ function consumerRoots(root, excludes) {
70
70
  return roots;
71
71
  }
72
72
 
73
- // Artifact names under `produce:` / `consume:` in a wagon document. Read line-wise
73
+ // Items under `produce:` / `consume:` in a wagon document: each artifact `name` with its
74
+ // declared consume source (`from:`) or produce destination (`to:`). Read line-wise
74
75
  // rather than with a YAML parser so this check carries no dependency the rest of the
75
76
  // family does not already have; the shape it reads is the one core's schema requires.
76
77
  export function wagonContract(text) {
77
78
  if (!/^wagon:\s*\S/m.test(text)) return null;
78
79
  const out = { produce: [], consume: [] };
79
- let section = null;
80
+ let section = null, item = null, itemIndent = -1;
80
81
  for (const line of text.split(/\r?\n/)) {
81
82
  const top = line.match(/^(\w+):\s*$/);
82
- if (top) { section = top[1] === "produce" || top[1] === "consume" ? top[1] : null; continue; }
83
- if (/^\S/.test(line)) { section = null; continue; }
83
+ if (top) { section = top[1] === "produce" || top[1] === "consume" ? top[1] : null; item = null; itemIndent = -1; continue; }
84
+ if (/^\S/.test(line)) { section = null; item = null; continue; }
84
85
  if (!section) continue;
85
- const name = line.match(/^\s*-?\s*name:\s*["']?([^"'#\s]+)["']?/);
86
- if (name) out[section].push(name[1]);
86
+ const dash = line.match(/^(\s*)-(?:\s|$)/);
87
+ if (dash && (itemIndent < 0 || dash[1].length === itemIndent)) {
88
+ itemIndent = dash[1].length;
89
+ item = { name: null, from: null, to: null };
90
+ out[section].push(item);
91
+ }
92
+ const field = line.match(/^\s*(?:-\s*)?(name|from|to):\s*["']?([^"'#\s]+)["']?/);
93
+ if (field && item && item[field[1]] === null) item[field[1]] = field[2];
87
94
  }
95
+ for (const kind of ["produce", "consume"]) out[kind] = out[kind].filter((entry) => entry.name);
88
96
  return out.produce.length || out.consume.length ? out : null;
89
97
  }
90
98
 
99
+ // A declared platform-service side: a consume `from: system:<service>` or a produce
100
+ // `to: system:<service>`. That side is storage, not Cargo, so it carries no Cargo move.
101
+ const systemSide = (end) => typeof end === "string" && /^system:[a-z][a-z0-9-]*$/.test(end);
102
+
91
103
  // Blank out COMMENTS, keeping string literals intact.
92
104
  //
93
105
  // The artifact name legitimately appears as a literal — `cargo.put("orders:confirmed-order")`
@@ -213,6 +225,14 @@ for (const root of parseJsonEnv("ATDD_SCAN_ROOTS", [])) {
213
225
  for (const step of doc.sequence) for (const end of [step?.from, step?.to]) if (typeof end === "string" && end.startsWith("wagon:")) carried.add(end.slice(6).trim());
214
226
  }
215
227
  if (!contracts.length) continue; // no declared contract
228
+ // Wagons that put an artifact into Cargo (any produce not written to system:) or take one
229
+ // from Cargo (any consume not read from system:). A system: label on the other side of such
230
+ // an artifact cannot relabel it: that artifact travels as Cargo.
231
+ const cargoProducers = new Map(), cargoConsumers = new Map();
232
+ for (const c of contracts) {
233
+ for (const p of c.produce) if (!systemSide(p.to)) cargoProducers.set(p.name, [...(cargoProducers.get(p.name) ?? []), c.wagon]);
234
+ for (const q of c.consume) if (!systemSide(q.from)) cargoConsumers.set(q.name, [...(cargoConsumers.get(q.name) ?? []), c.wagon]);
235
+ }
216
236
 
217
237
  const sources = walk(croot, (p) => TS.test(p) && !TEST.test(p), excludes)
218
238
  .map((file) => ({ file, text: maskComments(read(file)) }));
@@ -234,11 +254,30 @@ for (const root of parseJsonEnv("ATDD_SCAN_ROOTS", [])) {
234
254
  for (const artifact of moves.reads) reads.add(artifact);
235
255
  for (const artifact of moves.writes) writes.add(artifact);
236
256
  }
237
- for (const [kind, names] of [["produce", c.produce], ["consume", c.consume]]) {
238
- for (const name of names) {
257
+ const rel = c.file.startsWith(croot + sep) ? c.file.slice(croot.length + 1) : c.file;
258
+ for (const [kind, items] of [["produce", c.produce], ["consume", c.consume]]) {
259
+ for (const { name, from, to } of items) {
260
+ const side = kind === "produce" ? to : from;
261
+ if (systemSide(side)) {
262
+ const others = ((kind === "consume" ? cargoProducers : cargoConsumers).get(name) ?? []).filter((w) => w !== c.wagon);
263
+ if (others.length) {
264
+ const wagons = others.map((w) => `wagon:${w}`).join(", ");
265
+ violations.push({
266
+ rule_id: RULE, file: rel, line: 1, col: 0,
267
+ evidence: kind === "consume"
268
+ ? `the wagon declares it consumes "${name}" from ${side}, but ${wagons} produces it into Cargo; ` +
269
+ `a system: source cannot relabel Cargo traffic, so declare its wagon: source and read it through Cargo`
270
+ : `the wagon declares it produces "${name}" to ${side}, but ${wagons} consumes it through Cargo; ` +
271
+ `a system: destination cannot relabel Cargo traffic, so drop the system: destination and write it through Cargo`,
272
+ source_line: "",
273
+ });
274
+ continue;
275
+ }
276
+ // Storage, not Cargo: no Cargo move is owed, but the wagon must still be bound to its train step.
277
+ if (handlers.length) continue;
278
+ }
239
279
  const moved = kind === "produce" ? writes : reads;
240
280
  if (moved.has(name)) continue;
241
- const rel = c.file.startsWith(croot + sep) ? c.file.slice(croot.length + 1) : c.file;
242
281
  violations.push({
243
282
  rule_id: RULE, file: rel, line: 1, col: 0,
244
283
  evidence:
package/integrity.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.80",
2
+ "version": "0.10.82",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
@@ -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": "d16329bae24d94b41498003fa0a9636e31363d983022c9938fdc1e574fdfd8af",
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",
@@ -492,7 +492,7 @@
492
492
  "detectors/bun_interlocking_infrastructure/checks/interlocking_station_routing.mjs": "f3870fb3bacee46a6c3e10238284ef4d359182030d40d4f33b0f0ad465f7a284",
493
493
  "detectors/bun_interlocking_infrastructure/checks/journey_runner_boundary.mjs": "6284a8532722e5a6bff7e5184e24c7f273c1e63f1331204502e0e3704db5c86b",
494
494
  "detectors/bun_interlocking_infrastructure/checks/journey_station_routing.mjs": "66c09cdc17b099a55fa0ca894bd85a44706742bdd29caa5347f10e6487b4e724",
495
- "detectors/bun_interlocking_infrastructure/checks/wagon_contract_honoured.mjs": "79b5f824e7510f50e4925a98c01ef263290025748f64a27a11555f608d8001dc",
495
+ "detectors/bun_interlocking_infrastructure/checks/wagon_contract_honoured.mjs": "f6a7f8ef2e1d403934253b0491d3cb15451156ad7fa2b5b6ba313c0522a359b2",
496
496
  "detectors/bun_interlocking_infrastructure/detect.mjs": "6494579ec7b9d4680099401346337d73f4a52368ffb6b9d3b0fbca5dce2cde15",
497
497
  "detectors/bun_interlocking_infrastructure/fixtures/clean/interlocking_delegates/server.ts": "36913357fdd3c5e375829a0554c605d70f5c05e2e83349f47505741598f6eb15",
498
498
  "detectors/bun_interlocking_infrastructure/fixtures/clean/interlocking_delegates/src/match/wagon.ts": "ea202fa15fdf9d1391b1ae23343c9d1b09fcf8ef14f1dccfb8114ffaedea97c1",
@@ -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": "aaa0e6acdd73f1a8cfeea9a523a86c6d3133c020c37f97e7ec61db00f957ec6a",
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",
@@ -1253,7 +1253,7 @@
1253
1253
  "planner-schemas/train-interlocking-registry.schema.json": "fd741b5a3909d54cef225fffde80b04359416690a26aca5718b7d91027f3934b",
1254
1254
  "planner-schemas/train-interlocking.schema.json": "67914979977f23652298b5272adf3df298576ecafbff3e5084574848be8c33d8",
1255
1255
  "planner-schemas/train.schema.json": "d6fbac9067c1bffeb84eb79a71228441055af6e6e7ba141dd4e0bcb079345ec0",
1256
- "planner-schemas/wagon.schema.json": "57b98f1ade998d745538eb3d72fb3014a50042006870f90bce47028eea336eb4",
1256
+ "planner-schemas/wagon.schema.json": "12353e46d2216d2deef754615b629a696bd8a7bb35326379e7ef2b221a5ece3a",
1257
1257
  "planner-schemas/wmbt.schema.json": "7a3a5693bfcf01340667c2be4a3c48e74f34b1af7e8dc13372c0de06b7f4fe12",
1258
1258
  "relationships.yaml": "5860a308ebc3ee7fa8172fc2c6633984000ab3003f0ae4b6fa28db48879eda22",
1259
1259
  "src/agent.ts": "6d6c6e75908af8fbab13432f0001b0374bb25cf3dee46ba218dfd67e0090cb56",
@@ -1267,7 +1267,7 @@
1267
1267
  "src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
1268
1268
  "src/planner-kernel.ts": "5d3f5305fb59becb2cc97d8a03aca6b104f385805d91cc0410856e289dd6fbb1",
1269
1269
  "src/planner-schema-validator.ts": "ce529c0936171075e93c7539b3655fe895a98dd2973ebda5775958e6ebc316d9",
1270
- "src/planner-validators.ts": "99adeb65f579af923c11f21a8f55cfb9993b007fd8df59c5f764696b55379cfc",
1270
+ "src/planner-validators.ts": "bb300a5ca6ed6487359d680f9512359aa75c48205110dd46c33067db7b5a18a2",
1271
1271
  "src/profile-registry.ts": "5a7761d324181ae27deb51f198d63648a4c99e858b4de98b54d35e6cf8a39fe7",
1272
1272
  "src/ratchet.ts": "909b47124863287cbf6b02237b70fd50d82522d5029b2ae900c371d2ea2287a2",
1273
1273
  "src/register.ts": "6a2f1c6b8df0dc9c8f45df8ec9fac2be29044e048b08c8ffe423410e94dadedd",
@@ -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 [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.80",
3
+ "version": "0.10.82",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
@@ -85,9 +85,9 @@
85
85
  },
86
86
  "to": {
87
87
  "type": "string",
88
- "enum": ["internal", "external"],
88
+ "pattern": "^(internal|external|system:[a-z][a-z0-9-]*)$",
89
89
  "default": "external",
90
- "description": "Optional visibility: internal (private to this wagon), external (public to all wagons including self). Defaults to external when omitted."
90
+ "description": "Optional destination: internal (private to this wagon), external (public to all wagons including self), or system:service (written to a platform service, symmetric with consume from: system:service). Defaults to external when omitted."
91
91
  },
92
92
  "version": {
93
93
  "type": "string",
@@ -116,7 +116,8 @@ function contractRegistryFindings(root: string, wagons: PlanArtifact[], registry
116
116
  const name = text(produced.name), contract = text(produced.contract), producer = text(wagon.data.wagon);
117
117
  const consumers = (byName.get(name) ?? []).filter(consumer => consumer && consumer !== producer);
118
118
  if (consumers.length && !contract) findings.push(finding("planner.contract.registry-coherence", wagon.file, `produce ${name} has contract null but is consumed cross-wagon by ${consumers.sort().join(", ")}`));
119
- if (contract && !consumers.length && text(produced.to || "external") !== "external") findings.push(finding("planner.contract.registry-coherence", wagon.file, `produce ${name} declares ${contract} but has no cross-wagon consumer and is not marked to: external`));
119
+ const to = text(produced.to || "external");
120
+ if (contract && !consumers.length && to !== "external" && !/^system:[a-z][a-z0-9-]*$/.test(to)) findings.push(finding("planner.contract.registry-coherence", wagon.file, `produce ${name} declares ${contract} but has no cross-wagon consumer and is not marked to: external`));
120
121
  }
121
122
  return findings;
122
123
  }
@@ -297,7 +298,7 @@ export async function validateStaticPlannerConventions(root = process.cwd()): Pr
297
298
  for (const item of produced) {
298
299
  const contract = text(item.contract), telemetry = item.telemetry; if (contract) contractOwners.set(contract, [...(contractOwners.get(contract) ?? []), slug]);
299
300
  for (const value of Array.isArray(telemetry) ? telemetry.map(text) : [text(telemetry)]) if (value) telemetryOwners.set(value, [...(telemetryOwners.get(value) ?? []), slug]);
300
- const to = text(item.to || "external"); if (to !== "external" && to !== "internal" && (!to.startsWith("wagon:") || !wagonSlugs.has(to.slice(6)))) findings.push(finding("planner.wagon.produce-consume-artifacts", wagon.file, `${expected} produces to invalid destination ${to}`));
301
+ const to = text(item.to || "external"); if (to !== "external" && to !== "internal" && !/^system:[a-z][a-z0-9-]*$/.test(to) && (!to.startsWith("wagon:") || !wagonSlugs.has(to.slice(6)))) findings.push(finding("planner.wagon.produce-consume-artifacts", wagon.file, `${expected} produces to invalid destination ${to}`));
301
302
  }
302
303
  for (const item of consumed) {
303
304
  const from = text(item.from); if (!from) continue;