@afokapu/atdd-bun 0.6.0 → 0.6.2

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 (40) hide show
  1. package/README.md +87 -484
  2. package/conventions/atdd-bun.planner/atdd-bun.planner.acceptance-identity.convention.yaml +27 -0
  3. package/conventions/atdd-bun.planner/atdd-bun.planner.identity-required.convention.yaml +28 -0
  4. package/conventions/atdd-bun.planner/atdd-bun.planner.identity-unique.convention.yaml +27 -0
  5. package/conventions/atdd-bun.planner/atdd-bun.planner.interlocking-participant-resolves.convention.yaml +28 -0
  6. package/conventions/atdd-bun.planner/atdd-bun.planner.parse.convention.yaml +27 -0
  7. package/conventions/atdd-bun.planner/atdd-bun.planner.reference-resolves.convention.yaml +28 -0
  8. package/conventions/atdd-bun.planner/atdd-bun.planner.schema.convention.yaml +30 -0
  9. package/conventions/atdd-bun.planner/atdd-bun.planner.train-wagon-resolves.convention.yaml +26 -0
  10. package/conventions/atdd-bun.topology/atdd-bun.topology.e2e-location.convention.yaml +38 -0
  11. package/conventions/atdd-bun.topology/atdd-bun.topology.feature-source-coverage.convention.yaml +30 -0
  12. package/conventions/atdd-bun.topology/atdd-bun.topology.feature-test-coverage.convention.yaml +29 -0
  13. package/conventions/atdd-bun.topology/atdd-bun.topology.plan-location.convention.yaml +31 -0
  14. package/conventions/atdd-bun.topology/atdd-bun.topology.source-location.convention.yaml +33 -0
  15. package/conventions/atdd-bun.topology/atdd-bun.topology.test-location.convention.yaml +34 -0
  16. package/detectors/atdd_topology/atdd.implementation.yaml +3 -0
  17. package/detectors/atdd_topology/detect.mjs +14 -3
  18. package/detectors/atdd_topology/fixtures/clean/e2e/journeys/checkout.journey.test.ts +1 -0
  19. package/detectors/atdd_topology/fixtures/clean/plan/orders/E001.yaml +2 -0
  20. package/detectors/atdd_traceability_closure/detect.mjs +31 -16
  21. package/detectors/planner_docs_capability/atdd.implementation.yaml +7 -0
  22. package/detectors/planner_plan_integrity/atdd.implementation.yaml +1 -0
  23. package/detectors/planner_plan_integrity/detect.mjs +4 -1
  24. package/detectors/planner_plan_integrity/fixtures/dirty/plan/_trains/_interlockings/checkout.yaml +6 -0
  25. package/detectors/planner_plan_integrity/fixtures/dirty/plan/_trains/_interlockings/unnamed.yaml +2 -0
  26. package/detectors/planner_plan_integrity/fixtures/dirty/plan/_trains/dup-a.yaml +7 -0
  27. package/detectors/planner_plan_integrity/fixtures/dirty/plan/_trains/dup-b.yaml +6 -0
  28. package/detectors/planner_plan_integrity/fixtures/dirty/plan/orders/E001.yaml +5 -0
  29. package/detectors/planner_schema_validation/fixtures/dirty/plan/_trains/missing-description.yaml +5 -0
  30. package/integrity.json +39 -19
  31. package/package.json +1 -1
  32. package/planner-nodes/ENFORCEMENT_SCOPE.yaml +11 -1
  33. package/relationships.yaml +224 -0
  34. package/src/agent.ts +15 -8
  35. package/src/docs-capability.ts +3 -0
  36. package/src/enforce.ts +48 -2
  37. package/src/hooks.ts +2 -2
  38. package/src/integrity.ts +13 -5
  39. package/src/planner-kernel.ts +2 -1
  40. package/templates/agents/AGENTS.block.md +5 -1
@@ -0,0 +1,28 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.planner.identity-required
3
+ kind: rule
4
+ status: active
5
+ name: Plan artifacts carry their identity
6
+ statement: Every plan artifact whose kind the planner recognizes carries its canonical identity (`train_id`, `interlocking_id`,
7
+ `journey_id`, or `urn`). Package rule, strict.
8
+ terms:
9
+ - term_id: package_rule
10
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
11
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
12
+ relationship edges.'
13
+ content:
14
+ summary: Plan artifacts carry their identity.
15
+ normative_text: An artifact without an identity cannot be referenced, bound, or traced; every rule that resolves
16
+ references silently skips it.
17
+ fix_hint: 'Add the identity field for the artifact kind, for example `interlocking_id: interlocking:checkout`.'
18
+ exceptions:
19
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
20
+ metadata:
21
+ aliases:
22
+ - ATDD-BUN-PLANNER-IDENTITY-REQUIRED
23
+ severity: 3
24
+ disposition: strict
25
+ introduced_in: 0.1.0
26
+ implementation:
27
+ type: validator
28
+ ref: planner_plan_integrity
@@ -0,0 +1,27 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.planner.identity-unique
3
+ kind: rule
4
+ status: active
5
+ name: Plan identities are unique
6
+ statement: No two plan artifacts declare the same identity (`train_id`, `interlocking_id`, `journey_id`, `urn`,
7
+ …). Package rule, strict.
8
+ terms:
9
+ - term_id: package_rule
10
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
11
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
12
+ relationship edges.'
13
+ content:
14
+ summary: Plan identities are unique.
15
+ normative_text: A reference to a duplicated identity resolves to whichever file was read first.
16
+ fix_hint: Rename one of the artifacts, or merge them.
17
+ exceptions:
18
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
19
+ metadata:
20
+ aliases:
21
+ - ATDD-BUN-PLANNER-IDENTITY-UNIQUE
22
+ severity: 3
23
+ disposition: strict
24
+ introduced_in: 0.1.0
25
+ implementation:
26
+ type: validator
27
+ ref: planner_plan_integrity
@@ -0,0 +1,28 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.planner.interlocking-participant-resolves
3
+ kind: rule
4
+ status: active
5
+ name: Interlocking participants resolve
6
+ statement: Every participant and lifeline an interlocking lists is either an external actor (`user:…`, `system:…`)
7
+ or a typed reference to an artifact the plan declares. Package rule, strict.
8
+ terms:
9
+ - term_id: package_rule
10
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
11
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
12
+ relationship edges.'
13
+ content:
14
+ summary: Interlocking participants resolve.
15
+ normative_text: A routing decision between participants the plan does not declare cannot be traced to anything
16
+ that implements it.
17
+ fix_hint: Declare the participant, or correct the lifeline.
18
+ exceptions:
19
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
20
+ metadata:
21
+ aliases:
22
+ - ATDD-BUN-PLANNER-INTERLOCKING-PARTICIPANT-RESOLVES
23
+ severity: 3
24
+ disposition: strict
25
+ introduced_in: 0.1.0
26
+ implementation:
27
+ type: validator
28
+ ref: planner_plan_integrity
@@ -0,0 +1,27 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.planner.parse
3
+ kind: rule
4
+ status: active
5
+ name: Plan artifacts parse
6
+ statement: Every `.yaml`, `.yml` or `.json` file under `plan/` parses. Package rule, strict.
7
+ terms:
8
+ - term_id: package_rule
9
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
10
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
11
+ relationship edges.'
12
+ content:
13
+ summary: Plan artifacts parse.
14
+ normative_text: An artifact that does not parse is invisible to every other rule, so its defects hide behind one
15
+ error.
16
+ fix_hint: Fix the syntax the finding reports.
17
+ exceptions:
18
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
19
+ metadata:
20
+ aliases:
21
+ - ATDD-BUN-PLANNER-PARSE
22
+ severity: 3
23
+ disposition: strict
24
+ introduced_in: 0.1.0
25
+ implementation:
26
+ type: validator
27
+ ref: planner_plan_integrity
@@ -0,0 +1,28 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.planner.reference-resolves
3
+ kind: rule
4
+ status: active
5
+ name: Typed plan references resolve
6
+ statement: Every typed reference an artifact makes (`wagon:`, `feature:`, `wmbt:`, `train:`, `interlocking:`, `journey:`)
7
+ names an artifact the plan declares. `contract:` references are resolved by the contract registry rules instead.
8
+ Package rule, strict.
9
+ terms:
10
+ - term_id: package_rule
11
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
12
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
13
+ relationship edges.'
14
+ content:
15
+ summary: Typed plan references resolve.
16
+ normative_text: A reference to nothing is a relationship the plan claims and cannot show.
17
+ fix_hint: Declare the referenced artifact, or correct the reference.
18
+ exceptions:
19
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
20
+ metadata:
21
+ aliases:
22
+ - ATDD-BUN-PLANNER-REFERENCE-RESOLVES
23
+ severity: 3
24
+ disposition: strict
25
+ introduced_in: 0.1.0
26
+ implementation:
27
+ type: validator
28
+ ref: planner_plan_integrity
@@ -0,0 +1,30 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.planner.schema
3
+ kind: rule
4
+ status: active
5
+ name: Plan artifacts satisfy their package-shipped schema
6
+ statement: Every recognized plan artifact (wagon, feature, WMBT and its acceptances, train, train interlocking,
7
+ journey topology, registries) satisfies its JSON Schema in `planner-schemas/`. A violation that belongs to a canonical
8
+ rule is reported under that rule (for example `planner.train.naming`); every other violation is reported here.
9
+ Package rule, strict.
10
+ terms:
11
+ - term_id: package_rule
12
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
13
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
14
+ relationship edges.'
15
+ content:
16
+ summary: Plan artifacts satisfy their package-shipped schema.
17
+ normative_text: The schemas are the shape every other rule assumes. An artifact that breaks its schema breaks
18
+ those assumptions silently.
19
+ fix_hint: Correct the field the finding names, as the schema requires.
20
+ exceptions:
21
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
22
+ metadata:
23
+ aliases:
24
+ - ATDD-BUN-PLANNER-SCHEMA
25
+ severity: 3
26
+ disposition: strict
27
+ introduced_in: 0.1.0
28
+ implementation:
29
+ type: validator
30
+ ref: planner_schema_validation
@@ -0,0 +1,26 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.planner.train-wagon-resolves
3
+ kind: rule
4
+ status: active
5
+ name: Train wagons resolve
6
+ statement: Every wagon a train lists under `wagons:` is declared as `wagon:<slug>`. Package rule, strict.
7
+ terms:
8
+ - term_id: package_rule
9
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
10
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
11
+ relationship edges.'
12
+ content:
13
+ summary: Train wagons resolve.
14
+ normative_text: A train ordering a wagon that does not exist cannot run.
15
+ fix_hint: Declare the wagon, or remove it from the train.
16
+ exceptions:
17
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
18
+ metadata:
19
+ aliases:
20
+ - ATDD-BUN-PLANNER-TRAIN-WAGON-RESOLVES
21
+ severity: 3
22
+ disposition: strict
23
+ introduced_in: 0.1.0
24
+ implementation:
25
+ type: validator
26
+ ref: planner_plan_integrity
@@ -0,0 +1,38 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.topology.e2e-location
3
+ kind: rule
4
+ status: active
5
+ name: Every interlocking route and exposed journey has an end-to-end test at its canonical path
6
+ statement: 'Every interlocking route has `<e2e_root>/interlockings/<interlocking>/<route>.routes.test.ts`, and every
7
+ exposed journey `<e2e_root>/journeys/<journey>.journey.test.ts`, each carrying a `test:{wagon}:{feature}:{acceptance}`
8
+ URN for a declared feature and binding `Acceptance: acc:{wagon}:{acceptance}` declared by a WMBT. On an exposed
9
+ journey whose surfaces include `frontend`, a Playwright spec (`*.e2e.ts`) substitutes for the Bun test when it
10
+ is bound to the route''s train (`// Train:`) or the journey (`// Journey:`) and carries a valid E2E or SMOKE test
11
+ URN for that same subject (`test:train:<id>:E2E-NNN-…`, `test:journey:<id>:SMOKE-NNN-…`). A browser spec carries
12
+ its identity as that URN instead of an `Acceptance:` line, which `tester.htmx.journey-no-acceptance-marker` forbids
13
+ on journey specs. Package rule, strict.'
14
+ terms:
15
+ - term_id: package_rule
16
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
17
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
18
+ relationship edges.'
19
+ - term_id: topology_settings
20
+ text: the `topology:` keys `plan_root`, `source_root`, `test_root`, `e2e_root` in `atdd-bun.yaml` (defaults `plan`,
21
+ `src/wagons`, `tests/wagons`, `e2e`).
22
+ content:
23
+ summary: Every interlocking route and exposed journey has an end-to-end test at its canonical path.
24
+ normative_text: 'One behaviour, one end-to-end proof at a path derived from the plan: a missing proof is found
25
+ by path, not by searching.'
26
+ fix_hint: Add the test at the path the finding names, or, on a frontend journey, a Playwright spec bound with
27
+ `// Train:` or `// Journey:`.
28
+ exceptions:
29
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
30
+ metadata:
31
+ aliases:
32
+ - ATDD-BUN-TOPOLOGY-E2E-LOCATION
33
+ severity: 3
34
+ disposition: strict
35
+ introduced_in: 0.6.0
36
+ implementation:
37
+ type: validator
38
+ ref: atdd_topology
@@ -0,0 +1,30 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.topology.feature-source-coverage
3
+ kind: rule
4
+ status: active
5
+ name: A feature with WMBTs has source
6
+ statement: A feature that owns WMBTs has at least one source component beneath `<source_root>/<wagon>/features/<feature>/`.
7
+ Package rule, strict.
8
+ terms:
9
+ - term_id: package_rule
10
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
11
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
12
+ relationship edges.'
13
+ - term_id: topology_settings
14
+ text: the `topology:` keys `plan_root`, `source_root`, `test_root`, `e2e_root` in `atdd-bun.yaml` (defaults `plan`,
15
+ `src/wagons`, `tests/wagons`, `e2e`).
16
+ content:
17
+ summary: A feature with WMBTs has source.
18
+ normative_text: A feature with behaviour to deliver and no implementation is a gap the plan already names.
19
+ fix_hint: Implement the feature beneath its directory, or move the WMBTs to the feature that delivers them.
20
+ exceptions:
21
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
22
+ metadata:
23
+ aliases:
24
+ - ATDD-BUN-TOPOLOGY-FEATURE-SOURCE-COVERAGE
25
+ severity: 3
26
+ disposition: strict
27
+ introduced_in: 0.6.0
28
+ implementation:
29
+ type: validator
30
+ ref: atdd_topology
@@ -0,0 +1,29 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.topology.feature-test-coverage
3
+ kind: rule
4
+ status: active
5
+ name: A feature with WMBTs has a test bound to one of its acceptances
6
+ statement: A feature that owns WMBTs has at least one test bound to one of its acceptances. Package rule, strict.
7
+ terms:
8
+ - term_id: package_rule
9
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
10
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
11
+ relationship edges.'
12
+ - term_id: topology_settings
13
+ text: the `topology:` keys `plan_root`, `source_root`, `test_root`, `e2e_root` in `atdd-bun.yaml` (defaults `plan`,
14
+ `src/wagons`, `tests/wagons`, `e2e`).
15
+ content:
16
+ summary: A feature with WMBTs has a test bound to one of its acceptances.
17
+ normative_text: Behaviour declared and never tested is a silent gap.
18
+ fix_hint: Add a test beneath the feature bound to one of its acceptances.
19
+ exceptions:
20
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
21
+ metadata:
22
+ aliases:
23
+ - ATDD-BUN-TOPOLOGY-FEATURE-TEST-COVERAGE
24
+ severity: 3
25
+ disposition: strict
26
+ introduced_in: 0.6.0
27
+ implementation:
28
+ type: validator
29
+ ref: atdd_topology
@@ -0,0 +1,31 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.topology.plan-location
3
+ kind: rule
4
+ status: active
5
+ name: Wagons, features and WMBTs live at their canonical plan paths
6
+ statement: Every wagon is declared at `<plan_root>/<wagon>/_<wagon>.yaml`, every feature at `<plan_root>/<wagon>/<feature>.yaml`,
7
+ and every WMBT at `<plan_root>/<wagon>/<WMBT>.yaml`. Package rule, strict.
8
+ terms:
9
+ - term_id: package_rule
10
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
11
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
12
+ relationship edges.'
13
+ - term_id: topology_settings
14
+ text: the `topology:` keys `plan_root`, `source_root`, `test_root`, `e2e_root` in `atdd-bun.yaml` (defaults `plan`,
15
+ `src/wagons`, `tests/wagons`, `e2e`).
16
+ content:
17
+ summary: Wagons, features and WMBTs live at their canonical plan paths.
18
+ normative_text: The plan is navigated by path as much as by identity. A document at a non-canonical path is found
19
+ by nothing that walks the layout, and two layouts in one repository make every tool guess.
20
+ fix_hint: Move the document to the path the finding names.
21
+ exceptions:
22
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
23
+ metadata:
24
+ aliases:
25
+ - ATDD-BUN-TOPOLOGY-PLAN-LOCATION
26
+ severity: 3
27
+ disposition: strict
28
+ introduced_in: 0.6.0
29
+ implementation:
30
+ type: validator
31
+ ref: atdd_topology
@@ -0,0 +1,33 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.topology.source-location
3
+ kind: rule
4
+ status: active
5
+ name: Feature source lives beneath its feature and carries a component URN
6
+ statement: 'When the plan models at least one wagon, feature or WMBT, every source file beneath `<source_root>`
7
+ carries `URN: component:{wagon}:{feature}:{name}:{frontend|backend}:{domain|application|integration|presentation}`
8
+ and lives beneath `<source_root>/<wagon>/features/<feature>/`, for a feature the plan declares. Package rule,
9
+ strict.'
10
+ terms:
11
+ - term_id: package_rule
12
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
13
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
14
+ relationship edges.'
15
+ - term_id: topology_settings
16
+ text: the `topology:` keys `plan_root`, `source_root`, `test_root`, `e2e_root` in `atdd-bun.yaml` (defaults `plan`,
17
+ `src/wagons`, `tests/wagons`, `e2e`).
18
+ content:
19
+ summary: Feature source lives beneath its feature and carries a component URN.
20
+ normative_text: Source is traced to the feature that owns it by its URN and found by its path. Either missing,
21
+ the feature cannot be shown to have an implementation.
22
+ fix_hint: Add the component URN header and move the file beneath its feature's directory.
23
+ exceptions:
24
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
25
+ metadata:
26
+ aliases:
27
+ - ATDD-BUN-TOPOLOGY-SOURCE-LOCATION
28
+ severity: 3
29
+ disposition: strict
30
+ introduced_in: 0.6.0
31
+ implementation:
32
+ type: validator
33
+ ref: atdd_topology
@@ -0,0 +1,34 @@
1
+ schema_version: 1.1.0
2
+ rule_id: atdd-bun.topology.test-location
3
+ kind: rule
4
+ status: active
5
+ name: Feature tests live beneath their feature and bind a declared acceptance
6
+ statement: 'When the plan models at least one wagon, feature or WMBT, every test beneath `<test_root>` carries `URN:
7
+ test:{wagon}:{feature}:{acceptance}`, lives beneath `<test_root>/<wagon>/features/<feature>/{unit,contract,integration}/`,
8
+ and binds `Acceptance: acc:{wagon}:{acceptance}` declared by a WMBT that the test''s own feature lists. Package
9
+ rule, strict.'
10
+ terms:
11
+ - term_id: package_rule
12
+ text: 'a rule of this package''s own gates (`atdd-bun.*`), realized by a package guard rather than a canonical
13
+ planner node. It is held to the same per-rule checks as every convention: a convention, strict, a failing fixture,
14
+ relationship edges.'
15
+ - term_id: topology_settings
16
+ text: the `topology:` keys `plan_root`, `source_root`, `test_root`, `e2e_root` in `atdd-bun.yaml` (defaults `plan`,
17
+ `src/wagons`, `tests/wagons`, `e2e`).
18
+ content:
19
+ summary: Feature tests live beneath their feature and bind a declared acceptance.
20
+ normative_text: A test proves an acceptance only when it names one that exists; its path says which feature and
21
+ which kind of test it is.
22
+ fix_hint: Add the test URN and Acceptance header, and move the test beneath its feature under unit/, contract/
23
+ or integration/.
24
+ exceptions:
25
+ - Vendored and generated trees (`node_modules`, `dist`, `build`, `_generated`) are never scanned.
26
+ metadata:
27
+ aliases:
28
+ - ATDD-BUN-TOPOLOGY-TEST-LOCATION
29
+ severity: 3
30
+ disposition: strict
31
+ introduced_in: 0.6.0
32
+ implementation:
33
+ type: validator
34
+ ref: atdd_topology
@@ -10,4 +10,7 @@ emits_rule_ids:
10
10
  - atdd-bun.topology.e2e-location
11
11
  - atdd-bun.topology.feature-source-coverage
12
12
  - atdd-bun.topology.feature-test-coverage
13
+ realizes_convention:
14
+ - planner.wagon.features
15
+ - planner.feature.wagon-link
13
16
  entrypoint: detect.mjs
@@ -21,7 +21,7 @@ function add(rule_id, root, path, evidence, line = 1, source_line = "") { violat
21
21
  function walk(dir, predicate, found = []) {
22
22
  let entries; try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return found; }
23
23
  for (const entry of entries) {
24
- if (["node_modules", ".git", ".atdd", "dist", "build", ".next"].includes(entry.name)) continue;
24
+ if (["node_modules", ".git", ".atdd", "dist", "build", ".next", "_generated"].includes(entry.name)) continue;
25
25
  const path = join(dir, entry.name);
26
26
  if (entry.isDirectory()) walk(path, predicate, found); else if (predicate(path)) found.push(path);
27
27
  }
@@ -47,6 +47,10 @@ for (const root of roots) {
47
47
  const wmbts = docs.filter(doc => text(doc.data, "urn").startsWith("wmbt:")).map(doc => { const [, wagon, code] = text(doc.data, "urn").split(":"); return { ...doc, wagon, code, urn: text(doc.data, "urn") }; });
48
48
  const featureByUrn = new Map(features.map(feature => [feature.urn, feature])), memberships = new Map(), wmbtOwners = new Map(), sourceByFeature = new Map(), testByFeature = new Map();
49
49
  const acceptances = new Set(wmbts.flatMap(wmbt => list(wmbt.data, "acceptances").map(acceptance => text(acceptance?.identity, "urn")).filter(Boolean)));
50
+ // The feature(s) that own each acceptance, through the WMBTs they list: a test is credited only to its owner.
51
+ const acceptanceOwners = new Map();
52
+ for (const feature of features) for (const wmbtUrn of list(feature.data, "wmbts").map(String)) for (const wmbt of wmbts.filter(w => w.urn === wmbtUrn))
53
+ for (const acceptance of list(wmbt.data, "acceptances").map(a => text(a?.identity, "urn")).filter(Boolean)) acceptanceOwners.set(acceptance, [...(acceptanceOwners.get(acceptance) || []), `${feature.wagon}:${feature.slug}`]);
50
54
  const trains = new Set(docs.map(doc => text(doc.data, "train_id")).filter(Boolean));
51
55
 
52
56
  for (const wagon of wagons) {
@@ -95,6 +99,7 @@ for (const root of roots) {
95
99
  const binding = `acc:${wagon}:${acceptance}`;
96
100
  if (h.acceptance.value !== binding) add("atdd-bun.topology.test-location", root, path, `${h.urn.value} must bind Acceptance: ${binding}`, h.acceptance.line, h.acceptance.raw);
97
101
  else if (!acceptances.has(binding)) add("atdd-bun.topology.test-location", root, path, `${binding} is not declared by a WMBT in ${cfg.plan_root}/`, h.acceptance.line, h.acceptance.raw);
102
+ else if (acceptanceOwners.has(binding) && !acceptanceOwners.get(binding).includes(`${wagon}:${feature}`)) add("atdd-bun.topology.test-location", root, path, `${binding} belongs to feature:${acceptanceOwners.get(binding).join(", feature:")}, not feature:${wagon}:${feature}; a test lives beneath the feature whose acceptance it proves`, h.acceptance.line, h.acceptance.raw);
98
103
  else testByFeature.set(`${wagon}:${feature}`, [...(testByFeature.get(`${wagon}:${feature}`) || []), path]);
99
104
  }
100
105
  for (const feature of features) if (list(feature.data, "wmbts").length) {
@@ -111,8 +116,13 @@ for (const root of roots) {
111
116
  for (const path of walk(e2eRoot, path => /\.e2e\.[cm]?[jt]sx?$/.test(path))) {
112
117
  const content = read(path), h = header(path);
113
118
  if (!/from\s+["']@playwright\/test["']/.test(content) || !/(^|[^.\w])test(\.(describe|only|skip|fixme|fail|slow|step))?\s*\(/m.test(content)) continue;
114
- if (h.train.value) browserTrains.add(h.train.value);
115
- if (h.journey.value) browserJourneys.add(h.journey.value);
119
+ // A browser spec carries its journey identity as a test URN, not an Acceptance (tester.htmx forbids
120
+ // Acceptance on journey specs). It substitutes only when that URN is a valid E2E or SMOKE proof for the
121
+ // very train or journey it is bound to.
122
+ // The proof is the `// URN:` header, whole: a URN-shaped string elsewhere in the file (a decoy constant) proves nothing.
123
+ const proves = (subject) => h.urn.value.startsWith(`test:${subject}:`) && /^(E2E|SMOKE)-\d{3}-[a-z0-9][a-z0-9-]*$/.test(h.urn.value.slice(`test:${subject}:`.length));
124
+ if (h.train.value && proves(h.train.value)) browserTrains.add(h.train.value);
125
+ if (h.journey.value && proves(h.journey.value)) browserJourneys.add(h.journey.value);
116
126
  }
117
127
  const interlockingRoutes = new Set();
118
128
  const interlockings = new Map(docs.filter(doc => text(doc.data, "interlocking_id").startsWith("interlocking:")).map(doc => [text(doc.data, "interlocking_id"), doc]));
@@ -153,6 +163,7 @@ for (const root of roots) {
153
163
  else if (entry && typeof entry === "object" && entry.exposed === true && existsSync(path)) {
154
164
  const h = header(path), match = testUrn.exec(h.urn.value);
155
165
  if (!match || !declaredFeatures.has(`${match[1]}:${match[2]}`)) add("atdd-bun.topology.e2e-location", root, path, "journey E2E test requires a test:{wagon}:{feature}:{acceptance} URN naming a declared feature", h.urn.line, h.urn.raw);
166
+ else if (h.acceptance.value !== `acc:${match[1]}:${match[3]}` || !acceptances.has(h.acceptance.value)) add("atdd-bun.topology.e2e-location", root, path, `journey E2E test requires Acceptance: acc:${match[1]}:${match[3]}, declared by a WMBT in ${cfg.plan_root}/`, h.acceptance.line, h.acceptance.raw);
156
167
  if (!trains.has(h.train.value) || !journeyTrains.has(h.train.value)) add("atdd-bun.topology.e2e-location", root, path, `journey E2E Train: must resolve to a reachable selected train; found ${h.train.value || "<missing>"}`, h.train.line, h.train.raw);
157
168
  }
158
169
  }
@@ -1,4 +1,5 @@
1
1
  // URN: test:orders:place-order:E001-E2E-001
2
+ // Acceptance: acc:orders:E001-E2E-001
2
3
  // Train: train:orders:checkout
3
4
  import { test } from "bun:test";
4
5
  test("checkout", () => {});
@@ -2,5 +2,7 @@ urn: wmbt:orders:E001
2
2
  acceptances:
3
3
  - identity:
4
4
  urn: acc:orders:E001-UNIT-001
5
+ - identity:
6
+ urn: acc:orders:E001-E2E-001
5
7
  - identity:
6
8
  urn: acc:orders:E001-E2E-002
@@ -14,6 +14,7 @@ const testName = /(?:^test_.*|.*(?:\.test|\.spec|_test))\.(?:[cm]?[jt]sx?)$/;
14
14
  const violations = [];
15
15
  const plans = { acc: new Set(), wmbt: new Set(), train: new Set() };
16
16
  const tests = new Map();
17
+ const declaredAt = new Map();
17
18
  const sources = [];
18
19
  let planDisplay = "plan/";
19
20
 
@@ -32,10 +33,14 @@ function text(path) { try { return readFileSync(path, "utf8"); } catch { return
32
33
  function add(rule_id, file, line, evidence, source_line = "") {
33
34
  violations.push({ rule_id, file, line, col: 1, evidence, source_line });
34
35
  }
35
- function lineOf(content, token) { return content.slice(0, content.indexOf(token)).split("\n").length; }
36
+ function lineOf(content, token) { return lineAt(content, content.indexOf(token)); }
37
+ function lineAt(content, index) { return content.slice(0, index).split("\n").length; }
38
+ // The identity each binding header must carry.
39
+ const PREFIX = { Acceptance: "acc:", WMBT: "wmbt:", Train: "train:" };
36
40
 
37
41
  for (const root of roots) {
38
- const topology = await topologyFor(root), planPrefix = topology.planRoot.replace(/\/$/, "") + "/"; planDisplay = planPrefix;
42
+ // `plan_root: .` (or `./`) must still match root-relative paths, which never start with `./`.
43
+ const topology = await topologyFor(root), planDir = topology.planRoot.replace(/^\.(?:\/|$)/, "").replace(/\/$/, ""), planPrefix = planDir ? planDir + "/" : ""; planDisplay = planPrefix || "./";
39
44
  walk(root, (path) => {
40
45
  const name = path.split("/").pop() || "";
41
46
  const content = text(path);
@@ -45,43 +50,53 @@ for (const root of roots) {
45
50
  wmbt: /\bwmbt:[A-Za-z0-9_.:-]+/g,
46
51
  train: /\btrain:[A-Za-z0-9_.:-]+/g,
47
52
  })) for (const match of content.matchAll(re)) plans[kind].add(match[0]);
53
+ // Where each acceptance is DECLARED (its `urn:` line), so a missing test is reported on the artifact to fix.
54
+ for (const m of content.matchAll(/["']?\burn["']?[ \t]*:[ \t]*["']?(acc:[A-Za-z0-9_.:-]+)/g)) if (!declaredAt.has(m[1])) declaredAt.set(m[1], { path, line: lineAt(content, m.index) });
48
55
  return;
49
56
  }
50
57
  if (!sourceExtensions.has(name.slice(name.lastIndexOf(".")))) return;
51
58
  const head = content.split("\n").slice(0, 40).join("\n");
52
59
  if (testName.test(name)) {
53
60
  const urn = head.match(/^\s*\/\/\s*URN:\s*(test:[^\s]+)/m);
54
- const binding = head.match(/^\s*\/\/\s*(Acceptance|WMBT|Train):\s*((?:acc|wmbt|train):[^\s]+)/m);
55
61
  if (!urn) return;
56
- tests.set(urn[1], { path, binding: binding?.[2], bindingKind: binding?.[1] });
57
- if (!binding) add("traceability.test.binding-resolves", path, lineOf(head, urn[0]), "test has a URN but no Acceptance:, WMBT:, or Train: binding", urn[0]);
62
+ // EVERY binding header is judged, not only the first: a valid Acceptance: must not carry an unresolved Train:.
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
+ const bindings = headers.filter((h) => h.id.startsWith(PREFIX[h.kind]));
65
+ tests.set(urn[1], { path, bindings });
66
+ 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
+ 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);
58
68
  return;
59
69
  }
60
70
  const component = head.match(/^\s*\/\/\s*URN:\s*(component:[^\s]+)/m);
61
71
  if (!component) return;
62
- const testedBy = [...head.matchAll(/^\s*\/\/\s*-\s*(test:[^\s]+)/gm)].map((match) => match[1]);
72
+ // Only list entries directly under a `// Tested-By:` header count, and EVERY one of them is judged: a
73
+ // malformed `- not-a-test` after a valid entry fails too. A stray list item elsewhere declares nothing.
74
+ const testedBy = [];
75
+ let under = false;
76
+ for (const line of head.split("\n")) {
77
+ if (/^\s*\/\/\s*Tested-By:\s*$/.test(line)) { under = true; continue; }
78
+ const entry = under && line.match(/^\s*\/\/\s*-\s*(\S*)/);
79
+ if (entry) testedBy.push(entry[1] || "<empty>"); else under = false;
80
+ }
63
81
  sources.push({ path, head, component: component[1], testedBy });
64
82
  });
65
83
  }
66
84
 
67
- for (const test of tests.values()) {
68
- if (!test.binding) continue;
69
- const kind = test.binding.startsWith("acc:") ? "acc" : test.binding.startsWith("wmbt:") ? "wmbt" : "train";
70
- if (!plans[kind].has(test.binding)) {
71
- add("traceability.test.binding-resolves", test.path, lineOf(text(test.path), test.binding), `${test.binding} is not declared in plan/`, `// ${test.bindingKind}: ${test.binding}`);
72
- }
85
+ for (const test of tests.values()) for (const binding of test.bindings) {
86
+ if (!plans[binding.id.split(":", 1)[0]].has(binding.id)) add("traceability.test.binding-resolves", test.path, binding.line, `${binding.id} is not declared in plan/`, binding.raw);
73
87
  }
74
- const boundAcceptances = new Set([...tests.values()].map((test) => test.binding).filter((id) => id?.startsWith("acc:")));
88
+ const boundAcceptances = new Set([...tests.values()].flatMap((test) => test.bindings.map((binding) => binding.id)).filter((id) => id.startsWith("acc:")));
75
89
  for (const acceptance of plans.acc) {
76
- if (!boundAcceptances.has(acceptance)) add("traceability.plan.executable-acceptance-has-test", planDisplay, 1, `${acceptance} has no Bun test binding`, acceptance);
90
+ const at = declaredAt.get(acceptance);
91
+ if (!boundAcceptances.has(acceptance)) add("traceability.plan.executable-acceptance-has-test", at?.path ?? planDisplay, at?.line ?? 1, `${acceptance} has no Bun test binding`, acceptance);
77
92
  }
78
93
  for (const source of sources) {
79
- if (!source.testedBy.length) {
94
+ if (!source.testedBy.some((entry) => entry.startsWith("test:"))) {
80
95
  add("traceability.source.tested-by-present", source.path, lineOf(source.head, source.component), `${source.component} has no Tested-By: test:... declaration`, source.component);
81
96
  continue;
82
97
  }
83
98
  for (const testUrn of source.testedBy) {
84
- if (!tests.has(testUrn)) add("traceability.source.tested-by-resolves", source.path, lineOf(source.head, testUrn), `${testUrn} does not resolve to a Bun test`, testUrn);
99
+ if (!tests.has(testUrn)) add("traceability.source.tested-by-resolves", source.path, lineOf(source.head, testUrn), testUrn.startsWith("test:") ? `${testUrn} does not resolve to a Bun test` : `Tested-By entry ${testUrn} is not a test: URN`, testUrn);
85
100
  }
86
101
  }
87
102
  writeFileSync(report, JSON.stringify({ violations }, null, 2));
@@ -34,6 +34,13 @@ emits_rule_ids:
34
34
  - planner.docs.adr-registry-derived # disposition: advisory (registry drifted from :decides:)
35
35
  - planner.docs.journey-view-current # disposition: strict (generated journey view differs from plan/)
36
36
 
37
+ # Rules the capability API (checkDocumentation) emits and the filesystem report cannot: they need a
38
+ # declaration, a change set, or a render. tests/docs-capability.test.ts triggers each one.
39
+ api_emits_rule_ids:
40
+ - planner.docs.artifact-path-shape
41
+ - planner.docs.undeclared-change
42
+ - planner.docs.reference-integrity
43
+
37
44
  # TWO SURFACES, ONE DETECTOR.
38
45
  #
39
46
  # entrypoint the CAPABILITY seam. Core calls
@@ -9,6 +9,7 @@ contract_version: "1.0.0"
9
9
  # fail closed before convention validators can answer.
10
10
  emits_rule_ids:
11
11
  - atdd-bun.planner.parse
12
+ - atdd-bun.planner.identity-required
12
13
  - atdd-bun.planner.identity-unique
13
14
  - atdd-bun.planner.reference-resolves
14
15
  - atdd-bun.planner.train-wagon-resolves
@@ -6,6 +6,7 @@ const roots = JSON.parse(process.env.ATDD_SCAN_ROOTS ?? "[]"), report = process.
6
6
  if (!report) throw new Error("ATDD_VIOLATIONS_REPORT is required");
7
7
  const ids = {
8
8
  "planner.kernel.parse": "atdd-bun.planner.parse",
9
+ "planner.kernel.identity-required": "atdd-bun.planner.identity-required",
9
10
  "planner.kernel.identity-unique": "atdd-bun.planner.identity-unique",
10
11
  "planner.kernel.reference-resolves": "atdd-bun.planner.reference-resolves",
11
12
  "planner.kernel.train-wagon-resolves": "atdd-bun.planner.train-wagon-resolves",
@@ -14,7 +15,9 @@ const ids = {
14
15
  };
15
16
  const violations = (await Promise.all(roots.map(validatePlan))).flatMap(graph => graph.findings).map(item => ({
16
17
  ...item,
17
- rule_id: ids[item.rule_id] ?? item.rule_id,
18
+ // Every kernel finding reports under a declared package rule. An unmapped one is a new emission path:
19
+ // fail loudly rather than publish a rule id nothing declares (tests/rule-coverage.test.ts maps them all).
20
+ rule_id: ids[item.rule_id] ?? (() => { throw new Error(`planner_plan_integrity: kernel rule ${item.rule_id} has no declared package rule; map it in detect.mjs`); })(),
18
21
  line: 1,
19
22
  col: 1,
20
23
  source_line: "",
@@ -0,0 +1,6 @@
1
+ schema_version: 1.0.0
2
+ interlocking_id: interlocking:checkout
3
+ title: Checkout
4
+ lifelines:
5
+ - ref: wagon:nobody
6
+ routes: []