@afokapu/atdd-bun 0.10.81 → 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.
@@ -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.81",
2
+ "version": "0.10.82",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
@@ -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",
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.81",
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;