@afokapu/atdd-bun 0.6.2 → 0.7.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.
Files changed (92) hide show
  1. package/README.md +2 -0
  2. package/conventions/coder.bun/coder.bun.telemetry-forbidden-properties.convention.yaml +51 -0
  3. package/conventions/coder.bun/coder.bun.telemetry-implementation-binding.convention.yaml +45 -0
  4. package/conventions/coder.bun/coder.bun.telemetry-raw-string-emit.convention.yaml +53 -0
  5. package/conventions/coder.bun/coder.bun.telemetry-source-binding.convention.yaml +48 -0
  6. package/conventions/coder.bun/coder.bun.telemetry-vendor-sdk.convention.yaml +51 -0
  7. package/conventions/planner.telemetry/planner.telemetry.acceptance-decision.convention.yaml +58 -0
  8. package/conventions/planner.telemetry/planner.telemetry.logical-ownership.convention.yaml +50 -0
  9. package/conventions/planner.telemetry/planner.telemetry.metric-cardinality.convention.yaml +47 -0
  10. package/conventions/planner.telemetry/planner.telemetry.tracking-plan-schema.convention.yaml +75 -0
  11. package/conventions/tester.bun/tester.bun.telemetry-captured-sink.convention.yaml +47 -0
  12. package/conventions/tester.bun/tester.bun.telemetry-identity-assertion.convention.yaml +44 -0
  13. package/conventions/tester.bun/tester.bun.telemetry-required-item-coverage.convention.yaml +44 -0
  14. package/conventions/tester.bun/tester.bun.telemetry-test-binding.convention.yaml +52 -0
  15. package/conventions/tester.bun/tester.bun.telemetry-timing-semantics.convention.yaml +53 -0
  16. package/detectors/bun_telemetry_code/atdd.implementation.yaml +24 -0
  17. package/detectors/bun_telemetry_code/calls.mjs +104 -0
  18. package/detectors/bun_telemetry_code/checks/t_forbidden_properties.mjs +46 -0
  19. package/detectors/bun_telemetry_code/checks/t_implementation_binding.mjs +41 -0
  20. package/detectors/bun_telemetry_code/checks/t_raw_string_emit.mjs +34 -0
  21. package/detectors/bun_telemetry_code/checks/t_source_binding.mjs +48 -0
  22. package/detectors/bun_telemetry_code/checks/t_vendor_sdk.mjs +38 -0
  23. package/detectors/bun_telemetry_code/detect.mjs +50 -0
  24. package/detectors/bun_telemetry_code/fixtures/clean/plan/commons/E001.yaml +10 -0
  25. package/detectors/bun_telemetry_code/fixtures/clean/plan/commons/E002.yaml +7 -0
  26. package/detectors/bun_telemetry_code/fixtures/clean/plan/commons/_commons.yaml +6 -0
  27. package/detectors/bun_telemetry_code/fixtures/clean/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
  28. package/detectors/bun_telemetry_code/fixtures/clean/src/wagons/commons/features/ingress/infrastructure/otel-adapter.ts +8 -0
  29. package/detectors/bun_telemetry_code/fixtures/clean/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
  30. package/detectors/bun_telemetry_code/fixtures/clean/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
  31. package/detectors/bun_telemetry_code/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.telemetry.test.ts +13 -0
  32. package/detectors/bun_telemetry_code/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.test.ts +9 -0
  33. package/detectors/bun_telemetry_code/fixtures/dirty/plan/commons/E001.yaml +8 -0
  34. package/detectors/bun_telemetry_code/fixtures/dirty/plan/commons/_commons.yaml +6 -0
  35. package/detectors/bun_telemetry_code/fixtures/dirty/src/wagons/commons/features/ingress/domain/accept-response.ts +13 -0
  36. package/detectors/bun_telemetry_code/fixtures/dirty/src/wagons/commons/features/ingress/domain/tracing.ts +3 -0
  37. package/detectors/bun_telemetry_code/fixtures/dirty/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
  38. package/detectors/bun_telemetry_code/fixtures/dirty/tests/wagons/commons/features/ingress/unit/probe.test.ts +7 -0
  39. package/detectors/bun_telemetry_code/registry.mjs +34 -0
  40. package/detectors/bun_telemetry_test/_shared.mjs +92 -0
  41. package/detectors/bun_telemetry_test/atdd.implementation.yaml +24 -0
  42. package/detectors/bun_telemetry_test/checks/t_captured_sink.mjs +35 -0
  43. package/detectors/bun_telemetry_test/checks/t_identity_assertion.mjs +45 -0
  44. package/detectors/bun_telemetry_test/checks/t_required_item_coverage.mjs +37 -0
  45. package/detectors/bun_telemetry_test/checks/t_test_binding.mjs +56 -0
  46. package/detectors/bun_telemetry_test/checks/t_timing_semantics.mjs +51 -0
  47. package/detectors/bun_telemetry_test/detect.mjs +50 -0
  48. package/detectors/bun_telemetry_test/fixtures/clean/plan/commons/E001.yaml +10 -0
  49. package/detectors/bun_telemetry_test/fixtures/clean/plan/commons/E002.yaml +7 -0
  50. package/detectors/bun_telemetry_test/fixtures/clean/plan/commons/_commons.yaml +6 -0
  51. package/detectors/bun_telemetry_test/fixtures/clean/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
  52. package/detectors/bun_telemetry_test/fixtures/clean/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
  53. package/detectors/bun_telemetry_test/fixtures/clean/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
  54. package/detectors/bun_telemetry_test/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.telemetry.test.ts +13 -0
  55. package/detectors/bun_telemetry_test/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.test.ts +7 -0
  56. package/detectors/bun_telemetry_test/fixtures/dirty/plan/commons/E001.yaml +10 -0
  57. package/detectors/bun_telemetry_test/fixtures/dirty/plan/commons/E002.yaml +7 -0
  58. package/detectors/bun_telemetry_test/fixtures/dirty/plan/commons/_commons.yaml +6 -0
  59. package/detectors/bun_telemetry_test/fixtures/dirty/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
  60. package/detectors/bun_telemetry_test/fixtures/dirty/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
  61. package/detectors/bun_telemetry_test/fixtures/dirty/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
  62. package/detectors/bun_telemetry_test/fixtures/dirty/tests/wagons/commons/features/ingress/unit/dangling.telemetry.test.ts +9 -0
  63. package/detectors/bun_telemetry_test/fixtures/dirty/tests/wagons/commons/features/ingress/unit/loose.telemetry.test.ts +11 -0
  64. package/detectors/bun_telemetry_test/fixtures/dirty/tests/wagons/commons/features/ingress/unit/unbound.telemetry.test.ts +10 -0
  65. package/detectors/planner_telemetry_plan/atdd.implementation.yaml +22 -0
  66. package/detectors/planner_telemetry_plan/detect.mjs +9 -0
  67. package/detectors/planner_telemetry_plan/fixtures/clean/plan/commons/E001.yaml +10 -0
  68. package/detectors/planner_telemetry_plan/fixtures/clean/plan/commons/E002.yaml +7 -0
  69. package/detectors/planner_telemetry_plan/fixtures/clean/plan/commons/_commons.yaml +6 -0
  70. package/detectors/planner_telemetry_plan/fixtures/clean/src/wagons/commons/features/ingress/domain/accept-response.ts +8 -0
  71. package/detectors/planner_telemetry_plan/fixtures/clean/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
  72. package/detectors/planner_telemetry_plan/fixtures/clean/telemetry/commons/response-invocation-accepted/metric.be.duration.json +19 -0
  73. package/detectors/planner_telemetry_plan/fixtures/clean/tests/wagons/commons/features/ingress/unit/accept-response.telemetry.test.ts +13 -0
  74. package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/E001.yaml +9 -0
  75. package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/E002.yaml +4 -0
  76. package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/E003.yaml +7 -0
  77. package/detectors/planner_telemetry_plan/fixtures/dirty/plan/commons/_commons.yaml +6 -0
  78. package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/extra.json +1 -0
  79. package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/orphan-artifact/event.be.json +14 -0
  80. package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/response-invocation-accepted/event.be.json +16 -0
  81. package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/response-invocation-accepted/metric.be.duration.json +22 -0
  82. package/detectors/planner_telemetry_plan/fixtures/dirty/telemetry/commons/response-invocation-accepted/notes.json +1 -0
  83. package/integrity.json +91 -8
  84. package/lib/scan.mjs +37 -0
  85. package/package.json +1 -1
  86. package/planner-schemas/acceptance.schema.json +70 -1
  87. package/planner-schemas/telemetry-plan.schema.json +133 -0
  88. package/relationships.yaml +174 -0
  89. package/src/enforce.ts +2 -1
  90. package/src/index.ts +2 -0
  91. package/src/telemetry-plan.ts +223 -0
  92. package/src/topology.ts +3 -1
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "telemetry:event:be:commons:response-invocation-accepted",
3
+ "version": "1.0.0",
4
+ "logical_artifact": "telemetry:commons:response-invocation-accepted",
5
+ "kind": "event",
6
+ "plane": "be",
7
+ "owner": "commons",
8
+ "purpose": "Records that a response invocation passed acceptance so ingress health can be audited.",
9
+ "acceptances": ["acc:commons:E001-UNIT-001"],
10
+ "properties": {
11
+ "response_id": { "type": "string", "classification": "internal", "cardinality": "high" },
12
+ "outcome": { "type": "string", "allowed_values": ["accepted"] }
13
+ },
14
+ "required": ["response_id", "outcome"],
15
+ "forbidden_properties": ["raw_payload", "secret", "prompt"]
16
+ }
@@ -0,0 +1,7 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ test("a test file may reference undeclared names without tripping the coder rules", () => {
4
+ const sink = { emit: (_id: string) => {} };
5
+ sink.emit("fixture_undeclared_event");
6
+ expect(sink).toBeDefined();
7
+ });
@@ -0,0 +1,34 @@
1
+ // registry.mjs — the tracking-plan registry as the code-side checks see it.
2
+ //
3
+ // Shared by every member check so all four judge the SAME registry: the concrete
4
+ // item ids declared under the telemetry root, and each item's forbidden-property
5
+ // list (normalized, so a plan's raw_payload meets code's rawPayload). Loaded once
6
+ // per detector run from the package's own loader, which also carries the adoption
7
+ // gate: until the repository adopts the capability, every check emits nothing.
8
+ import { loadTelemetryFiles, CONCRETE_URN } from "../../src/telemetry-plan.ts";
9
+
10
+ /** Property names cross vocabulary boundaries: the plan writes snake_case, code writes camelCase. */
11
+ export const normalizeName = (name) => name.replace(/[_-]/g, "").toLowerCase();
12
+
13
+ let cache = null;
14
+ export async function registry() {
15
+ if (cache) return cache;
16
+ const roots = JSON.parse(process.env.ATDD_SCAN_ROOTS ?? "[]");
17
+ const loaded = await Promise.all(roots.map((root) => loadTelemetryFiles(root)));
18
+ const ids = new Set();
19
+ const forbidden = new Map();
20
+ const itemFileById = new Map();
21
+ for (const { files } of loaded) {
22
+ for (const file of files) {
23
+ if (!file.data) continue;
24
+ const id = typeof file.data.id === "string" ? file.data.id : "";
25
+ if (!id) continue;
26
+ ids.add(id);
27
+ itemFileById.set(id, file.file);
28
+ const names = Array.isArray(file.data.forbidden_properties) ? file.data.forbidden_properties.filter((name) => typeof name === "string") : [];
29
+ forbidden.set(id, new Set(names.map(normalizeName)));
30
+ }
31
+ }
32
+ cache = { adopted: loaded.some((result) => result.adopted), ids, forbidden, requiredIds: new Set(loaded.flatMap((result) => [...result.requiredIds])), itemFileById, concreteUrn: CONCRETE_URN };
33
+ return cache;
34
+ }
@@ -0,0 +1,92 @@
1
+ // _shared.mjs — registry view, test-file walking and header parsing for the
2
+ // telemetry test family. Never spawned as a check (the runner skips _-prefixed
3
+ // files in checks/; this lives at the implementation root).
4
+ //
5
+ // A TELEMETRY TEST is a test file identified by a `// Telemetry:` header, by a URN
6
+ // kind segment (-TELEMETRY- / -EVENT- / -METRIC-), or by *.telemetry.test.*
7
+ // colocation — the same identification tester.bun.telemetry-emit uses, plus the
8
+ // binding header this profile adds.
9
+ import { readFileSync, statSync, readdirSync } from "node:fs";
10
+ import { join, extname, sep } from "node:path";
11
+ import { loadTelemetryFiles, CONCRETE_URN } from "../../src/telemetry-plan.ts";
12
+
13
+ const TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
14
+ const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
15
+
16
+ function isExcluded(path, excludes) {
17
+ const segs = path.split(sep);
18
+ return excludes.some((ex) => segs.includes(ex) || path.includes(ex));
19
+ }
20
+
21
+ export function* walkTests(root, excludes) {
22
+ let st;
23
+ try { st = statSync(root); } catch { return; }
24
+ if (st.isFile()) { if (TEST_RE.test(root)) yield root; return; }
25
+ let names;
26
+ try { names = readdirSync(root); } catch { return; }
27
+ for (const name of names) {
28
+ const full = join(root, name);
29
+ if (isExcluded(full, excludes)) continue;
30
+ let cst;
31
+ try { cst = statSync(full); } catch { continue; }
32
+ if (cst.isDirectory()) yield* walkTests(full, excludes);
33
+ else if (TEST_RE.test(full)) yield full;
34
+ }
35
+ }
36
+
37
+ const STRING_ON_LINE = /(["'`])(?:\\.|(?!\1)[^\n\\])*\1/g;
38
+ const TELEMETRY_HEADER = /^[ \t]*\/\/[ \t]*Telemetry:[ \t]*(\S+)[ \t]*$/;
39
+ const URN_HEADER = /^[ \t]*\/\/[ \t]*URN:[ \t]*(\S+)[ \t]*$/;
40
+ const ACCEPTANCE_HEADER = /^[ \t]*\/\/[ \t]*Acceptance:[ \t]*(\S+)[ \t]*$/;
41
+
42
+ /** The header facts a telemetry test is judged on: its URN, EVERY acceptance binding (a spec
43
+ * file legitimately covers many acceptances — the two-level @covers model), and every Telemetry:
44
+ * reference (with line numbers, judged one per line). String literals are stripped first: a
45
+ * header inside a string is not a header. */
46
+ export function parseTestHeader(text) {
47
+ const lines = text.split("\n");
48
+ const header = { urn: null, acceptances: [], telemetry: [] };
49
+ for (let no = 0; no < lines.length; no++) {
50
+ const line = lines[no].replace(STRING_ON_LINE, '""');
51
+ const asTelemetry = TELEMETRY_HEADER.exec(line);
52
+ if (asTelemetry) { header.telemetry.push({ no: no + 1, value: asTelemetry[1], raw: lines[no].trim() }); continue; }
53
+ if (!header.urn) { const m = URN_HEADER.exec(line); if (m) { header.urn = { no: no + 1, value: m[1], raw: lines[no].trim() }; continue; } }
54
+ const asAcceptance = ACCEPTANCE_HEADER.exec(line);
55
+ if (asAcceptance) header.acceptances.push({ no: no + 1, value: asAcceptance[1], raw: lines[no].trim() });
56
+ }
57
+ return header;
58
+ }
59
+
60
+ const IS_TELEMETRY_URN = /-(?:TELEMETRY|EVENT|METRIC)-\d+/i;
61
+
62
+ /** Is this test file a telemetry test? */
63
+ export function isTelemetryTest(file, header) {
64
+ if (header.telemetry.length) return true;
65
+ if (/\.telemetry\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file)) return true;
66
+ return Boolean(header.urn && IS_TELEMETRY_URN.test(header.urn.value));
67
+ }
68
+
69
+ export function readText(file) {
70
+ try { return readFileSync(file, "utf8"); } catch { return null; }
71
+ }
72
+
73
+ let cache = null;
74
+ export async function registry() {
75
+ if (cache) return cache;
76
+ const roots = JSON.parse(process.env.ATDD_SCAN_ROOTS ?? "[]");
77
+ const loaded = await Promise.all(roots.map((root) => loadTelemetryFiles(root)));
78
+ const ids = new Set(), itemFileById = new Map(), timingById = new Map();
79
+ const requiredIds = new Set(), decisions = new Map();
80
+ for (const result of loaded) {
81
+ for (const id of result.requiredIds) requiredIds.add(id);
82
+ for (const [id, decision] of result.decisions) decisions.set(id, decision);
83
+ for (const file of result.files) {
84
+ if (!file.data || typeof file.data.id !== "string" || !file.data.id) continue;
85
+ ids.add(file.data.id);
86
+ itemFileById.set(file.data.id, file.file);
87
+ timingById.set(file.data.id, Array.isArray(file.data.timing) ? file.data.timing.filter((t) => typeof t === "string") : []);
88
+ }
89
+ }
90
+ cache = { adopted: loaded.some((result) => result.adopted), ids, itemFileById, timingById, requiredIds, decisions, concreteUrn: CONCRETE_URN };
91
+ return cache;
92
+ }
@@ -0,0 +1,24 @@
1
+ schema_version: "1.0.0"
2
+ kind: implementation
3
+ subtype: validator
4
+ implementation_id: tester.telemetry.impl
5
+ targets_workspace: atdd.workspace.bun
6
+ contract_version: "1.0.0"
7
+ # The test-side half of the telemetry profile: telemetry tests bind the acceptance
8
+ # AND the telemetry item they prove, assert the exact identity on a captured sink,
9
+ # every required item has at least one telemetry test, and declared timing
10
+ # semantics are exercised. Inert until the repository adopts the capability.
11
+ emits_rule_ids:
12
+ - tester.bun.telemetry-test-binding
13
+ - tester.bun.telemetry-captured-sink
14
+ - tester.bun.telemetry-identity-assertion
15
+ - tester.bun.telemetry-required-item-coverage
16
+ - tester.bun.telemetry-timing-semantics
17
+ realizes_convention:
18
+ - tester.bun.telemetry-test-binding
19
+ - tester.bun.telemetry-captured-sink
20
+ - tester.bun.telemetry-identity-assertion
21
+ - tester.bun.telemetry-required-item-coverage
22
+ - tester.bun.telemetry-timing-semantics
23
+ entrypoint: detect.mjs
24
+ report: detect.mjs
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bun
2
+ // Member check: tester.bun.telemetry-captured-sink (telemetry test family)
3
+ //
4
+ // CONTRACT (v1.1): reads ATDD_SCAN_ROOTS / ATDD_SCAN_EXCLUDES, writes RAW violations
5
+ // to ATDD_VIOLATIONS_REPORT, exits 0 regardless of count.
6
+ import { readRoots, parseJsonEnv, emit } from "../../../lib/scan.mjs";
7
+ import { registry, walkTests, parseTestHeader, isTelemetryTest, readText } from "../_shared.mjs";
8
+
9
+ // A telemetry test asserts on the SINK, not the return value: toHaveBeenCalled*
10
+ // on the emitted spy, or an expectation on .emit/.emitted/.capture/.track/.record.
11
+ // Same predicate family as tester.bun.telemetry-emit, scoped to this profile and
12
+ // joined with the exact-identity requirement of tester.bun.telemetry-identity-assertion.
13
+ const RULE = "tester.bun.telemetry-captured-sink";
14
+ const EMISSION_ASSERTION = /toHaveBeenCalled(?:With|Times)?\s*\(|expect[\s\S]{0,160}?\.\s*(?:emit|emitted|capture|track|record)\b/;
15
+ const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
16
+
17
+ const { adopted } = await registry();
18
+ const violations = [];
19
+ if (adopted) {
20
+ const excludes = [...DEFAULT_EXCLUDES, ...parseJsonEnv("ATDD_SCAN_EXCLUDES", [])];
21
+ for (const root of readRoots()) {
22
+ for (const file of walkTests(root, excludes)) {
23
+ const text = readText(file);
24
+ if (!text) continue;
25
+ const header = parseTestHeader(text);
26
+ if (!isTelemetryTest(file, header)) continue;
27
+ if (EMISSION_ASSERTION.test(text)) continue;
28
+ violations.push({ rule_id: RULE, file, line: header.urn ? header.urn.no : 1, col: 1,
29
+ evidence: "telemetry test asserts no emission (toHaveBeenCalled / expect on .emit/.capture/.track); a return-value check proves the function ran, not that the item fired",
30
+ source_line: header.urn ? header.urn.raw : "" });
31
+ }
32
+ }
33
+ }
34
+ process.stderr.write(`bun-telemetry-test[captured-sink]: ${violations.length} violation(s)\n`);
35
+ emit(violations);
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bun
2
+ // Member check: tester.bun.telemetry-identity-assertion (telemetry test family)
3
+ //
4
+ // CONTRACT (v1.1): reads ATDD_SCAN_ROOTS / ATDD_SCAN_EXCLUDES, writes RAW violations
5
+ // to ATDD_VIOLATIONS_REPORT, exits 0 regardless of count.
6
+ import { readRoots, parseJsonEnv, emit, maskComments } from "../../../lib/scan.mjs";
7
+ import { registry, walkTests, parseTestHeader, isTelemetryTest, readText } from "../_shared.mjs";
8
+
9
+ // Exact identity: the concrete URN the test claims to prove must appear in the
10
+ // test's CODE (a string literal in an assertion), not only in its header. A test
11
+ // that mocks an emitter and asserts "called with anything" proves emission
12
+ // happened, not that the planned item is the one that fired.
13
+ const RULE = "tester.bun.telemetry-identity-assertion";
14
+ const ANY_URN_STRING = /["'`]telemetry:[^"'`\n]+["'`]/;
15
+ const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
16
+
17
+ const { adopted } = await registry();
18
+ const violations = [];
19
+ if (adopted) {
20
+ const excludes = [...DEFAULT_EXCLUDES, ...parseJsonEnv("ATDD_SCAN_EXCLUDES", [])];
21
+ for (const root of readRoots()) {
22
+ for (const file of walkTests(root, excludes)) {
23
+ const text = readText(file);
24
+ if (!text) continue;
25
+ const header = parseTestHeader(text);
26
+ if (!isTelemetryTest(file, header)) continue;
27
+ const code = maskComments(text); // comments do not assert; string literals do
28
+ const claimed = header.telemetry.map((reference) => reference.value);
29
+ if (claimed.length) {
30
+ for (const urn of claimed) {
31
+ if (code.includes(`"${urn}"`) || code.includes(`'${urn}'`) || code.includes("`" + urn + "`")) continue;
32
+ violations.push({ rule_id: RULE, file, line: 1, col: 1,
33
+ evidence: `never asserts the exact identity ${urn}; the URN must appear in an assertion string, not only in the header`,
34
+ source_line: "" });
35
+ }
36
+ } else if (!ANY_URN_STRING.test(code)) {
37
+ violations.push({ rule_id: RULE, file, line: header.urn ? header.urn.no : 1, col: 1,
38
+ evidence: "telemetry test asserts no concrete telemetry identity; assert the exact telemetry: URN the test claims to prove",
39
+ source_line: header.urn ? header.urn.raw : "" });
40
+ }
41
+ }
42
+ }
43
+ }
44
+ process.stderr.write(`bun-telemetry-test[identity-assertion]: ${violations.length} violation(s)\n`);
45
+ emit(violations);
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bun
2
+ // Member check: tester.bun.telemetry-required-item-coverage (telemetry test family)
3
+ //
4
+ // CONTRACT (v1.1): reads ATDD_SCAN_ROOTS / ATDD_SCAN_EXCLUDES, writes RAW violations
5
+ // to ATDD_VIOLATIONS_REPORT, exits 0 regardless of count.
6
+ import { readRoots, parseJsonEnv, emit } from "../../../lib/scan.mjs";
7
+ import { registry, walkTests, parseTestHeader, readText } from "../_shared.mjs";
8
+
9
+ // The closure from the other side: every item an acceptance REQUIRES must have at
10
+ // least one telemetry test bound to it by a Telemetry: header. The implementation
11
+ // binding is the coder rule's concern; this is the evidence side — planned telemetry
12
+ // that no test proves is a plan nobody can rely on.
13
+ const RULE = "tester.bun.telemetry-required-item-coverage";
14
+ const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
15
+
16
+ const { adopted, ids, requiredIds, itemFileById } = await registry();
17
+ const violations = [];
18
+ if (adopted) {
19
+ const excludes = [...DEFAULT_EXCLUDES, ...parseJsonEnv("ATDD_SCAN_EXCLUDES", [])];
20
+ const bound = new Set();
21
+ for (const root of readRoots()) {
22
+ for (const file of walkTests(root, excludes)) {
23
+ const text = readText(file);
24
+ if (!text) continue;
25
+ for (const reference of parseTestHeader(text).telemetry) bound.add(reference.value);
26
+ }
27
+ }
28
+ for (const id of [...requiredIds].sort()) {
29
+ // An unresolvable required URN is the acceptance-decision rule's finding; judge only declared items.
30
+ if (!ids.has(id) || bound.has(id)) continue;
31
+ violations.push({ rule_id: RULE, file: itemFileById.get(id) ?? "telemetry/", line: 1, col: 1,
32
+ evidence: `${id} is required by an acceptance but no telemetry test binds it with a // Telemetry: header`,
33
+ source_line: "" });
34
+ }
35
+ }
36
+ process.stderr.write(`bun-telemetry-test[required-item-coverage]: ${violations.length} violation(s)\n`);
37
+ emit(violations);
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bun
2
+ // Member check: tester.bun.telemetry-test-binding (telemetry test family)
3
+ //
4
+ // CONTRACT (v1.1): reads ATDD_SCAN_ROOTS / ATDD_SCAN_EXCLUDES, writes RAW violations
5
+ // to ATDD_VIOLATIONS_REPORT, exits 0 regardless of count.
6
+ import { readRoots, parseJsonEnv, emit } from "../../../lib/scan.mjs";
7
+ import { registry, walkTests, parseTestHeader, readText } from "../_shared.mjs";
8
+
9
+ // A telemetry test proves an acceptance's required item, and says so in its header:
10
+ // the Telemetry: URN must resolve into the tracking plan, and at least ONE of the test's
11
+ // Acceptance: bindings must be an acceptance whose decision requires that item — a spec
12
+ // file legitimately covers many acceptances, and any of them may be the requiring one.
13
+ const RULE = "tester.bun.telemetry-test-binding";
14
+ const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
15
+
16
+ const { adopted, ids, decisions, concreteUrn } = await registry();
17
+ const violations = [];
18
+ if (adopted) {
19
+ const excludes = [...DEFAULT_EXCLUDES, ...parseJsonEnv("ATDD_SCAN_EXCLUDES", [])];
20
+ for (const root of readRoots()) {
21
+ for (const file of walkTests(root, excludes)) {
22
+ const text = readText(file);
23
+ if (!text) continue;
24
+ const header = parseTestHeader(text);
25
+ for (const reference of header.telemetry) {
26
+ const at = { line: reference.no, col: 1, source_line: reference.raw };
27
+ if (!concreteUrn.test(reference.value)) {
28
+ violations.push({ rule_id: RULE, file, ...at, evidence: `${reference.value} is not a concrete telemetry URN telemetry:{kind}:{plane}:{theme}:{artifact}[:{measure}]` });
29
+ continue;
30
+ }
31
+ if (!ids.has(reference.value)) {
32
+ violations.push({ rule_id: RULE, file, ...at, evidence: `${reference.value} does not resolve to a tracking-plan item under telemetry/` });
33
+ continue;
34
+ }
35
+ if (!header.acceptances.length) {
36
+ violations.push({ rule_id: RULE, file, ...at, evidence: `binds telemetry but no Acceptance: a telemetry test proves an acceptance's required item` });
37
+ continue;
38
+ }
39
+ const names = header.acceptances.map((binding) => binding.value);
40
+ const declared = header.acceptances.filter((binding) => decisions.has(binding.value));
41
+ if (!declared.length) {
42
+ violations.push({ rule_id: RULE, file, line: header.acceptances[0].no, col: 1, source_line: header.acceptances[0].raw, evidence: `${names.join(", ")} — none is a declared acceptance in the plan` });
43
+ continue;
44
+ }
45
+ if (!declared.some((binding) => {
46
+ const decision = decisions.get(binding.value);
47
+ return decision.disposition === "required" && decision.urns.includes(reference.value);
48
+ })) {
49
+ violations.push({ rule_id: RULE, file, ...at, evidence: `${names.join(", ")} — none requires ${reference.value}; bind a test to an acceptance whose telemetry decision lists the item` });
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ process.stderr.write(`bun-telemetry-test[test-binding]: ${violations.length} violation(s)\n`);
56
+ emit(violations);
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bun
2
+ // Member check: tester.bun.telemetry-timing-semantics (telemetry test family)
3
+ //
4
+ // CONTRACT (v1.1): reads ATDD_SCAN_ROOTS / ATDD_SCAN_EXCLUDES, writes RAW violations
5
+ // to ATDD_VIOLATIONS_REPORT, exits 0 regardless of count.
6
+ import { readRoots, parseJsonEnv, emit, maskComments } from "../../../lib/scan.mjs";
7
+ import { registry, walkTests, parseTestHeader, readText } from "../_shared.mjs";
8
+
9
+ // Critical timing semantics are DECLARED on the tracking-plan item (timing:) and
10
+ // exercised by the tests bound to it: post-commit, absent-after-rollback,
11
+ // correlation-across-async. This is deliberately plan-driven: the check cannot
12
+ // prove ordering statically, so it requires the plan to say when ordering matters
13
+ // and the bound tests to reference the behaviour — in code or a test title, never
14
+ // in a comment. Diagnostic logs and internal spans that declare no timing are not
15
+ // judged.
16
+ const RULE = "tester.bun.telemetry-timing-semantics";
17
+ const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
18
+ const SEMANTIC_PROBES = {
19
+ "post-commit": /\bcommits?\b/i,
20
+ "absent-after-rollback": /\brollback\b|\brolled\s+back\b/i,
21
+ "correlation-across-async": /\bcorrelation\b|\bcontext\b/i,
22
+ };
23
+
24
+ const { adopted, ids, timingById, itemFileById } = await registry();
25
+ const violations = [];
26
+ if (adopted) {
27
+ const excludes = [...DEFAULT_EXCLUDES, ...parseJsonEnv("ATDD_SCAN_EXCLUDES", [])];
28
+ const testsByItem = new Map();
29
+ for (const root of readRoots()) {
30
+ for (const file of walkTests(root, excludes)) {
31
+ const text = readText(file);
32
+ if (!text) continue;
33
+ for (const reference of parseTestHeader(text).telemetry) {
34
+ testsByItem.set(reference.value, [...(testsByItem.get(reference.value) ?? []), maskComments(text)]);
35
+ }
36
+ }
37
+ }
38
+ for (const [id, semantics] of [...timingById].sort(([left], [right]) => left.localeCompare(right))) {
39
+ if (!ids.has(id) || !semantics.length) continue;
40
+ const boundTests = testsByItem.get(id) ?? [];
41
+ for (const semantic of semantics) {
42
+ const probe = SEMANTIC_PROBES[semantic];
43
+ if (probe && boundTests.some((text) => probe.test(text))) continue;
44
+ violations.push({ rule_id: RULE, file: itemFileById.get(id) ?? "telemetry/", line: 1, col: 1,
45
+ evidence: `${id} declares timing semantic '${semantic}' but no bound telemetry test exercises it; assert the emission happens ${semantic === "post-commit" ? "only after commit" : semantic === "absent-after-rollback" ? "never after rollback" : "with correlation context carried across the async handoff"}`,
46
+ source_line: "" });
47
+ }
48
+ }
49
+ }
50
+ process.stderr.write(`bun-telemetry-test[timing-semantics]: ${violations.length} violation(s)\n`);
51
+ emit(violations);
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bun
2
+ // FAMILY validator: bun_telemetry_test
3
+ // Runs each member check (checks/*.mjs) VERBATIM as a subprocess and merges their
4
+ // RAW v1.1 reports into one — one implementation realizing a family of rule_ids.
5
+ // Files whose name begins with `_` are skipped: they are shared helpers.
6
+ import { execFileSync } from "node:child_process";
7
+ import { readFileSync, writeFileSync, mkdtempSync, readdirSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join, dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ const reportPath = process.env.ATDD_VIOLATIONS_REPORT;
14
+ if (!reportPath) {
15
+ process.stderr.write("family: ATDD_VIOLATIONS_REPORT is not set\n");
16
+ process.exit(2);
17
+ }
18
+ const checks = readdirSync(join(here, "checks"))
19
+ .filter((f) => (f.endsWith(".mjs") || f.endsWith(".ts")) && !f.startsWith("_"))
20
+ .sort();
21
+ const td = mkdtempSync(join(tmpdir(), "atdd-bun-fam-"));
22
+ const out = [];
23
+ let crashed = false;
24
+ for (const c of checks) {
25
+ const rep = join(td, c + ".json");
26
+ let exitedCleanly = true;
27
+ try {
28
+ execFileSync(process.execPath, [join(here, "checks", c)], {
29
+ env: { ...process.env, ATDD_VIOLATIONS_REPORT: rep },
30
+ stdio: ["ignore", "ignore", "inherit"],
31
+ });
32
+ } catch {
33
+ exitedCleanly = false; /* a member may exit non-zero after writing its report */
34
+ }
35
+ let read = false;
36
+ try {
37
+ out.push(...JSON.parse(readFileSync(rep, "utf8")).violations);
38
+ read = true;
39
+ } catch {}
40
+ // A member that crashed WITHOUT a report is a detector bug, and swallowing it here is a silent
41
+ // pass — the exact failure that once hid a broken regex behind zero findings. Fail loudly.
42
+ if (!exitedCleanly && !read) {
43
+ process.stderr.write(`family bun_telemetry_test: member ${c} crashed without a violation report\n`);
44
+ crashed = true;
45
+ }
46
+ }
47
+ if (crashed) process.exit(2); // a crashed member is a detector bug: fail loudly, not silently
48
+ writeFileSync(reportPath, JSON.stringify({ violations: out }, null, 2), "utf8");
49
+ process.stderr.write("family bun_telemetry_test: " + out.length + " violation(s)\n");
50
+ process.exit(0);
@@ -0,0 +1,10 @@
1
+ urn: wmbt:commons:E001
2
+ acceptances:
3
+ - identity:
4
+ urn: acc:commons:E001-UNIT-001
5
+ telemetry:
6
+ disposition: required
7
+ events:
8
+ - telemetry:event:be:commons:response-invocation-accepted
9
+ metrics:
10
+ - telemetry:metric:be:commons:response-invocation-accepted:duration
@@ -0,0 +1,7 @@
1
+ urn: wmbt:commons:E002
2
+ acceptances:
3
+ - identity:
4
+ urn: acc:commons:E002-UNIT-001
5
+ telemetry:
6
+ disposition: not-applicable
7
+ rationale: No externally useful observable outcome beyond the tested return value.
@@ -0,0 +1,6 @@
1
+ urn: wagon:commons
2
+ wagon: commons
3
+ produce:
4
+ - name: commons:response-invocation
5
+ contract: null
6
+ telemetry: telemetry:commons:response-invocation-accepted
@@ -0,0 +1,8 @@
1
+ // Telemetry: telemetry:event:be:commons:response-invocation-accepted
2
+ // Telemetry: telemetry:metric:be:commons:response-invocation-accepted:duration
3
+ export type TelemetryPort = { emit(id: string, properties: Record<string, unknown>): void };
4
+
5
+ export function acceptResponse(response: { id: string }, telemetry: TelemetryPort): void {
6
+ telemetry.emit("telemetry:event:be:commons:response-invocation-accepted", { response_id: response.id, outcome: "accepted" });
7
+ telemetry.emit("telemetry:metric:be:commons:response-invocation-accepted:duration", { outcome: "accepted" });
8
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "telemetry:event:be:commons:response-invocation-accepted",
3
+ "version": "1.0.0",
4
+ "logical_artifact": "telemetry:commons:response-invocation-accepted",
5
+ "kind": "event",
6
+ "plane": "be",
7
+ "owner": "commons",
8
+ "purpose": "Records that a response invocation passed acceptance so ingress health can be audited.",
9
+ "acceptances": ["acc:commons:E001-UNIT-001"],
10
+ "properties": {
11
+ "response_id": { "type": "string", "classification": "internal", "cardinality": "high" },
12
+ "outcome": { "type": "string", "allowed_values": ["accepted"] }
13
+ },
14
+ "required": ["response_id", "outcome"],
15
+ "forbidden_properties": ["raw_payload", "secret", "prompt"]
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "telemetry:metric:be:commons:response-invocation-accepted:duration",
3
+ "version": "1.0.0",
4
+ "logical_artifact": "telemetry:commons:response-invocation-accepted",
5
+ "kind": "metric",
6
+ "plane": "be",
7
+ "measure": "duration",
8
+ "instrument": "histogram",
9
+ "unit": "ms",
10
+ "owner": "commons",
11
+ "purpose": "Bounds how long an accepted response invocation takes to complete end to end.",
12
+ "acceptances": ["acc:commons:E001-UNIT-001"],
13
+ "properties": {
14
+ "outcome": { "type": "string", "classification": "internal", "cardinality": "low" }
15
+ },
16
+ "required": ["outcome"],
17
+ "dimensions": [{ "name": "outcome", "cardinality": "low" }],
18
+ "timing": ["post-commit"]
19
+ }
@@ -0,0 +1,13 @@
1
+ // URN: test:commons:ingress:E001-UNIT-001
2
+ // Acceptance: acc:commons:E001-UNIT-001
3
+ // Telemetry: telemetry:event:be:commons:response-invocation-accepted
4
+ // Telemetry: telemetry:metric:be:commons:response-invocation-accepted:duration
5
+ import { expect, mock, test } from "bun:test";
6
+ import { acceptResponse } from "../../../../../src/wagons/commons/features/ingress/domain/accept-response";
7
+
8
+ test("emits the planned event once the invocation commits", () => {
9
+ const emit = mock(() => {});
10
+ acceptResponse({ id: "r-1" }, { emit });
11
+ expect(emit).toHaveBeenCalledWith("telemetry:event:be:commons:response-invocation-accepted", { response_id: "r-1", outcome: "accepted" });
12
+ expect(emit).toHaveBeenCalledWith("telemetry:metric:be:commons:response-invocation-accepted:duration", { outcome: "accepted" });
13
+ });
@@ -0,0 +1,7 @@
1
+ // URN: test:commons:ingress:E002-UNIT-001
2
+ // Acceptance: acc:commons:E002-UNIT-001
3
+ import { expect, test } from "bun:test";
4
+
5
+ test("a plain non-telemetry test is not judged by the telemetry rules", () => {
6
+ expect(1 + 1).toBe(2);
7
+ });
@@ -0,0 +1,10 @@
1
+ urn: wmbt:commons:E001
2
+ acceptances:
3
+ - identity:
4
+ urn: acc:commons:E001-UNIT-001
5
+ telemetry:
6
+ disposition: required
7
+ events:
8
+ - telemetry:event:be:commons:response-invocation-accepted
9
+ metrics:
10
+ - telemetry:metric:be:commons:response-invocation-accepted:duration
@@ -0,0 +1,7 @@
1
+ urn: wmbt:commons:E002
2
+ acceptances:
3
+ - identity:
4
+ urn: acc:commons:E002-UNIT-001
5
+ telemetry:
6
+ disposition: not-applicable
7
+ rationale: No externally useful observable outcome beyond the tested return value.
@@ -0,0 +1,6 @@
1
+ urn: wagon:commons
2
+ wagon: commons
3
+ produce:
4
+ - name: commons:response-invocation
5
+ contract: null
6
+ telemetry: telemetry:commons:response-invocation-accepted
@@ -0,0 +1,8 @@
1
+ // Telemetry: telemetry:event:be:commons:response-invocation-accepted
2
+ // Telemetry: telemetry:metric:be:commons:response-invocation-accepted:duration
3
+ export type TelemetryPort = { emit(id: string, properties: Record<string, unknown>): void };
4
+
5
+ export function acceptResponse(response: { id: string }, telemetry: TelemetryPort): void {
6
+ telemetry.emit("telemetry:event:be:commons:response-invocation-accepted", { response_id: response.id, outcome: "accepted" });
7
+ telemetry.emit("telemetry:metric:be:commons:response-invocation-accepted:duration", { outcome: "accepted" });
8
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "telemetry:event:be:commons:response-invocation-accepted",
3
+ "version": "1.0.0",
4
+ "logical_artifact": "telemetry:commons:response-invocation-accepted",
5
+ "kind": "event",
6
+ "plane": "be",
7
+ "owner": "commons",
8
+ "purpose": "Records that a response invocation passed acceptance so ingress health can be audited.",
9
+ "acceptances": ["acc:commons:E001-UNIT-001"],
10
+ "properties": {
11
+ "response_id": { "type": "string", "classification": "internal", "cardinality": "high" },
12
+ "outcome": { "type": "string", "allowed_values": ["accepted"] }
13
+ },
14
+ "required": ["response_id", "outcome"],
15
+ "forbidden_properties": ["raw_payload", "secret", "prompt"]
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "telemetry:metric:be:commons:response-invocation-accepted:duration",
3
+ "version": "1.0.0",
4
+ "logical_artifact": "telemetry:commons:response-invocation-accepted",
5
+ "kind": "metric",
6
+ "plane": "be",
7
+ "measure": "duration",
8
+ "instrument": "histogram",
9
+ "unit": "ms",
10
+ "owner": "commons",
11
+ "purpose": "Bounds how long an accepted response invocation takes to complete end to end.",
12
+ "acceptances": ["acc:commons:E001-UNIT-001"],
13
+ "properties": {
14
+ "outcome": { "type": "string", "classification": "internal", "cardinality": "low" }
15
+ },
16
+ "required": ["outcome"],
17
+ "dimensions": [{ "name": "outcome", "cardinality": "low" }],
18
+ "timing": ["post-commit"]
19
+ }