@afokapu/atdd-bun 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -55,7 +55,7 @@ registerEnforcementTest({ root: import.meta.dir + "/..", profiles: ["traceabilit
55
55
  | `topology` | feature decomposition and the plan, source, test and E2E locations |
56
56
  | `planner` | schemas for every plan artifact, graph integrity, the scoped planner rules |
57
57
  | `telemetry` | the telemetry tracking plan: item shape, path-mirrored identity and versioning under `telemetry/`, wagon ownership of logical artifacts, the per-acceptance telemetry decision, metric label cardinality, source `Telemetry:` references, raw-string and forbidden-property emission, the vendor-SDK boundary around the TelemetryPort, and telemetry tests that bind the acceptance and item, assert the exact identity on a captured sink, cover every required item, and exercise declared timing semantics |
58
- | `delivery` | the review record of each tranche under `delivery/`: allowed author and reviewer models with recorded fallbacks, reviewer independence, every finding fixed, withdrawn after one dispute or ruled on by a human, every configured stage approved, and, at the gate, no change without a record and a merged head that contains exactly the approved commit. Inert until adopted |
58
+ | `delivery` | the review record of each tranche under `docs/delivery/tranches/`: allowed author and reviewer models with recorded fallbacks, reviewer independence, every finding fixed, withdrawn after one dispute or ruled on by a human, every configured stage approved, and, at the gate, no change without a record and a merged head that contains exactly the approved commit. Inert until adopted |
59
59
  | `docs` | the documentation capability, including the generated journey view |
60
60
  | `coder`, `tester`, `security`, `architecture`, `metrics`, `runtime` | Bun source and test conventions |
61
61
  | `interlocking` | train/interlocking binding, infrastructure and route coverage |
@@ -115,12 +115,23 @@ For programs delivered as tranches by a coordinator and persistent drivers, with
115
115
  and independent reviewers. Adopt it by naming `delivery` in `profiles:` (or, with no list, by adding
116
116
  a `delivery:` block); `agent init` then installs the delivery skill and its review contract. The
117
117
  adopting pull request is itself governed: it changes files outside the delivery root, so it carries
118
- its own tranche record, reviewed and `ready` like any other. Every key is optional; these are the
119
- defaults:
118
+ its own tranche record, reviewed and `ready` like any other.
119
+
120
+ The records live with the program's reasoning, in the docs profile's `docs/delivery/` area:
121
+
122
+ ```text
123
+ docs/delivery/index.adoc the program: why, scope, how it was split (docs profile)
124
+ docs/delivery/tranches/<tranche>/evidence.yaml one tranche's review record (delivery profile)
125
+ docs/delivery/tranches/<tranche>/*.json the retained raw reviewer reports
126
+ ```
127
+
128
+ Where delivery is adopted, the docs profile leaves the records folder to the delivery profile: its
129
+ YAML and reports are not authored documentation, and changing them needs no docs declaration. Every
130
+ key is optional; these are the defaults:
120
131
 
121
132
  ```yaml
122
133
  delivery:
123
- root: delivery # one <tranche>/evidence.yaml per tranche, reports beside it
134
+ root: docs/delivery/tranches # one <tranche>/evidence.yaml per tranche, reports beside it
124
135
  require_record: true # at the gate, a change outside the root needs a tranche record
125
136
  multiplexer: herdr # the terminal multiplexer agents run in; any command name
126
137
  independence: fresh-process # or different-model; overridable per stage
@@ -4,7 +4,7 @@ kind: rule
4
4
  status: active
5
5
  name: The delivery policy in atdd-bun.yaml is well-formed
6
6
  statement: >-
7
- The `delivery:` block of atdd-bun.yaml validates against delivery-config.schema.json: known keys only, each stage listing at least one reviewer, models named in lowercase, independence one of fresh-process or different-model, the root in canonical form (no leading, trailing or doubled slash, no dot segment) and not overlapping the plan, source, test, e2e or telemetry root (REQUIRED).
7
+ The `delivery:` block of atdd-bun.yaml validates against delivery-config.schema.json: known keys only, each stage listing at least one reviewer, models named in lowercase, independence one of fresh-process or different-model, the root in canonical form (no leading, trailing or doubled slash, no dot segment) not overlapping the plan, source, test, e2e or telemetry root, and, inside docs/, exactly docs/delivery/tranches (REQUIRED).
8
8
  terms:
9
9
  - term_id: policy
10
10
  text: >-
@@ -18,7 +18,7 @@ content:
18
18
  Correct the key the finding names. The defaults are:
19
19
 
20
20
  delivery:
21
- root: delivery
21
+ root: docs/delivery/tranches
22
22
  independence: fresh-process
23
23
  stages:
24
24
  plan_review: { authors: [codex], reviewers: [glm, claude] }
@@ -4,7 +4,7 @@ kind: rule
4
4
  status: active
5
5
  name: Every tranche folder holds a well-formed evidence record
6
6
  statement: >-
7
- Every folder under the delivery root holds an evidence.yaml that validates against delivery-evidence.schema.json, whose tranche matches the folder name, whose reviews each name an author and a configured stage, and whose reports are data files (json, jsonl, yaml, yml, txt, md, log) inside the tranche's own folder, one per review; a ready record names one for every review (delivery.stages-complete) (REQUIRED).
7
+ Every folder under the delivery root holds an evidence.yaml that validates against delivery-evidence.schema.json, whose tranche matches the folder name, whose reviews each name an author and a configured stage, and whose reports are data files (json, jsonl, yaml, yml, txt, md, log) inside the tranche's own folder, one per review; a ready record names one for every review (delivery.stages-complete); and the records folder holds nothing else: a data file belongs only as a report a record in its tranche names, and every other file, including a loose file directly under the root, is a finding (REQUIRED).
8
8
  terms:
9
9
  - term_id: tranche
10
10
  text: >-
@@ -21,7 +21,7 @@ content:
21
21
  Create or repair `<root>/<tranche>/evidence.yaml`; the finding names the schema path at fault. A request-changes review carries at least one finding; every review carries its `checked` list.
22
22
  exceptions:
23
23
  - >-
24
- Files other than evidence.yaml inside a tranche folder (retained raw reviewer reports) are not judged.
24
+ The default root, docs/delivery/tranches, sits in the docs profile's delivery area beside the program's reasoning (docs/delivery/index.adoc); the docs profile leaves the folder's record and data files to this profile, and AsciiDoc there stays documentation.
25
25
  metadata:
26
26
  aliases:
27
27
  - DELIVERY-EVIDENCE-SCHEMA-001
@@ -4,7 +4,7 @@ kind: rule
4
4
  status: active
5
5
  name: A tranche merges only its approved SHA
6
6
  statement: >-
7
- At the gate (ATDD_DELIVERY_GATE, which the generated CI sets to merge on pull requests and the merge queue and to post-merge on pushes to the protected branches), every evidence record the change touches is ready and approves a commit the head contains; before the merge the head differs from that commit only by the record and the reports it names; no record is deleted, and no record or report already on the base branch is modified; under the delivery root only records and the reports a changed record names change; and, with require_record (the default), a change outside the delivery root comes with a tranche record (REQUIRED).
7
+ At the gate (ATDD_DELIVERY_GATE, which the generated CI sets to merge on pull requests and the merge queue and to post-merge on pushes to the protected branches), every evidence record the change touches is ready and approves a commit the head contains; before the merge the head differs from that commit only by the record and the reports it names; no record is deleted, and no record or report already on the base branch is modified (records 0.8.0 kept under delivery/ included, after the root moves); under the delivery root only records and the reports a changed record names change; and, with require_record (the default), a change outside the delivery root comes with a tranche record (REQUIRED).
8
8
  terms:
9
9
  - term_id: merge_gate
10
10
  text: >-
@@ -45,6 +45,7 @@ content:
45
45
  migration preserves history and never promotes it.
46
46
  exceptions:
47
47
  - Everything beneath `docs/dist/` is generated output and is never scanned.
48
+ - Where the delivery profile is adopted, its records folder (delivery.root, by default `docs/delivery/tranches/`) holds tranche records and raw reviewer reports, not authored documentation; its record and data files are left to that profile, while AsciiDoc there stays documentation.
48
49
  - Markdown OUTSIDE `docs/` is untouched by this rule. `README.md`, `CONTRIBUTING.md` and the
49
50
  repository-root corpus are out of its scope; they are migrated by the adoption step, not
50
51
  rejected by this detector.
@@ -50,6 +50,7 @@ content:
50
50
  the honest correction.
51
51
  exceptions:
52
52
  - '`docs/dist/**` is generated output and is never required to be declared.'
53
+ - Where the delivery profile is adopted, its records folder (delivery.root, by default `docs/delivery/tranches/`) changes with every tranche; its record and data files are governed by that profile's merge gate, not by a documentation declaration (AsciiDoc there is still documentation and still declared).
53
54
  - A repository with no documentation declaration recorded at all is core's concern (companion §4,
54
55
  check 1), not this rule's. This rule fires when a declaration exists and does not cover the diff.
55
56
  - >-
package/integrity.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.8.0",
2
+ "version": "0.9.0",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
6
- "README.md": "e39b94cf795cd3b50b9ee88283beaef1e4d73568a5da2cf2959ea02a98d97afb",
6
+ "README.md": "f21a04f8688ce6bcc4d2dbf065eb24e886971546871935067c6f96314418312f",
7
7
  "bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
8
8
  "conventions/atdd-bun.planner/atdd-bun.planner.acceptance-identity.convention.yaml": "81c5c773d5ee15e8d99a2c237aa9846b110b2533df45ebf407d0e3ec6ed3dd03",
9
9
  "conventions/atdd-bun.planner/atdd-bun.planner.identity-required.convention.yaml": "e96d7c1455d0c221072d82e7f2b55da1c4f9ceb17718eb6ed96affdeef37675b",
@@ -92,23 +92,23 @@
92
92
  "conventions/coder.htmx/coder.htmx.verb-endpoint-same-origin.convention.yaml": "2128d26153e2d1f19c65a1e1665d4f82c78395daedb336d6ef1256e82b74aedb",
93
93
  "conventions/coder.htmx/coder.htmx.verb-mutation-signals-progress.convention.yaml": "ae85c5ea01a09a3c41b939988088a8e5da91848d2710a1f0a1e0c86308638ac9",
94
94
  "conventions/delivery/delivery.approved-sha-resolves.convention.yaml": "8bad8a0fdb28c4dc5f22b8949c8113eec4cc7b18b18af8c12ead5e3e320384ac",
95
- "conventions/delivery/delivery.config-schema.convention.yaml": "da60ccaed20fb8c77eb8adeb2999158ac3790c6bd28f2e2b2f69e75badbc34fa",
96
- "conventions/delivery/delivery.evidence-schema.convention.yaml": "ecd838a69c53a84018bfc937dce5a80877f5251172b498e418e7d1d3db46ea43",
95
+ "conventions/delivery/delivery.config-schema.convention.yaml": "6c37e1eef9d955f8dadd1308c9641fa5842529d207e7576284a3bc21a076da71",
96
+ "conventions/delivery/delivery.evidence-schema.convention.yaml": "d835dc07f029ffa01ad2dd5b129c45863efb2883b7fe0139eef8ac0bce68cc08",
97
97
  "conventions/delivery/delivery.findings-resolved.convention.yaml": "f6574e93a9a2d4bdd6d6f5645720b1a647fc0048d95f7675fe783d2ddb62c05b",
98
- "conventions/delivery/delivery.merge-gate.convention.yaml": "dab8c893999d13798d99a9b3ecd350f47de651365bc9d05b9be57a87aecf84ce",
98
+ "conventions/delivery/delivery.merge-gate.convention.yaml": "5961a599a750bd0137f10a25b69b4ba8210c5d3022a02c258c100c4716553172",
99
99
  "conventions/delivery/delivery.model-allowed.convention.yaml": "0476fb4a00dfdfbc16ac3c5843d7796d03e9d7c3ebd2c898eddb47160ac2fcdb",
100
100
  "conventions/delivery/delivery.reviewer-independent.convention.yaml": "f3388fe27a0b50549c913ab56a3afda7b70e54f8d0c32146ff59bd9da76afcfc",
101
101
  "conventions/delivery/delivery.stages-complete.convention.yaml": "1fd8c0a57e8ee2f91104088c25ee2000fb83e15b58a0cc5c432b37fa0457d056",
102
102
  "conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml": "0ea4232beba90280c664f93d097cafa44c67c2919c1468d4823fdc9e686aa5d2",
103
103
  "conventions/planner.docs/planner.docs.area-index-required.convention.yaml": "fd931e9f225bfbb5e78436d0d8a2a89cc0f5114493579962c72dd971301becce",
104
104
  "conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml": "a9f6178d29abe9385342a46a2c57470a3631aaf5e26f01e3660b7088b042418e",
105
- "conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml": "6e7b5b16efd794bee677d0827adc71c2f66df8bcba0264dd74a25c67c19f3a3b",
105
+ "conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml": "9e6fadbbe9aaf6015afc479f5b392e4c4324bfc47e1c59e315d26c2e5180a5f8",
106
106
  "conventions/planner.docs/planner.docs.doc-id-unique.convention.yaml": "558bc7d7001645f5443fcf19a5da67df17220b335d99811d103529bf22555311",
107
107
  "conventions/planner.docs/planner.docs.graph-target-resolves.convention.yaml": "657225233b92125b2667e2d72d65dc0ebc7cfc92b2c9e023d3d42449281683ea",
108
108
  "conventions/planner.docs/planner.docs.identity-required.convention.yaml": "86427664a497d7fefc8f77178cf64db66cb7ce328c1a3d2b94a0109ee7401e86",
109
109
  "conventions/planner.docs/planner.docs.journey-view-current.convention.yaml": "5892d4213d72342dc568c37f13942139bbdce4c0d5716075de579dc8060fc969",
110
110
  "conventions/planner.docs/planner.docs.reference-integrity.convention.yaml": "34310088114ac7cc58bc74443a76366e3e644121aaa3ff39b6602a0d15e03293",
111
- "conventions/planner.docs/planner.docs.undeclared-change.convention.yaml": "102a0706b00e074a74dbe38a6a389075929aa12bc38dcfb155d7a5d52afe6abc",
111
+ "conventions/planner.docs/planner.docs.undeclared-change.convention.yaml": "eb0df8fbf343ed38fe4694f63a065ee3e0e060d819f57ca8ea2f151aef66a8d5",
112
112
  "conventions/planner.telemetry/planner.telemetry.acceptance-decision.convention.yaml": "67dc840e0993c6940a431403b47439484ed557855a7dee82d75abd1479f3151e",
113
113
  "conventions/planner.telemetry/planner.telemetry.logical-ownership.convention.yaml": "6dd666a22fd7fd0cb6e5fe5505665c8889a504ee929fdebf3aee019df4aac2b4",
114
114
  "conventions/planner.telemetry/planner.telemetry.metric-cardinality.convention.yaml": "e61831f207ad4f09be983b300cbedc40f6ad08a4b2483612fc195e2343be93f6",
@@ -665,12 +665,12 @@
665
665
  "detectors/delivery_evidence/atdd.implementation.yaml": "2f6fc30dc353bef97bcb32ea54e4317d3c87cbd7ae1443fcef2aefc46280dc09",
666
666
  "detectors/delivery_evidence/detect.mjs": "e98664dee8090a0b280cd285a54e9a6fb9ba3572af2666b4e3656ca5b0555509",
667
667
  "detectors/delivery_evidence/fixtures/clean/atdd-bun.yaml": "f701861e3530bb5a25a70d73e248549fb96642f48db1ff4d3e8e4546aaa825ee",
668
- "detectors/delivery_evidence/fixtures/clean/delivery/api/evidence.yaml": "2cdbc4f628cf2d56627e6692b1f4e1a9b72316a3b7d09b47fffcd54c696a0fcb",
668
+ "detectors/delivery_evidence/fixtures/clean/docs/delivery/tranches/api/evidence.yaml": "2cdbc4f628cf2d56627e6692b1f4e1a9b72316a3b7d09b47fffcd54c696a0fcb",
669
669
  "detectors/delivery_evidence/fixtures/dirty/atdd-bun.yaml": "85c572d65f27b11b7ff8f115883913b2d2f1978097c416d3341b9c9d0de373d4",
670
- "detectors/delivery_evidence/fixtures/dirty/delivery/api/evidence.yaml": "2b848f160a401a217f56d53d87ce8e0a0efc0ed6d3781f6cc746e384c93eb8cc",
671
- "detectors/delivery_evidence/fixtures/dirty/delivery/data/evidence.yaml": "165dd67ff64f14c30268de5b18bf0c9c6634fb9ca48439c660c1962cebe3e0cb",
672
- "detectors/delivery_evidence/fixtures/dirty/delivery/orphan/README.md": "7d480b1fff6546615e55d866784bc2267d06d0221dfde8d0ba0353fa1ea0db9a",
673
- "detectors/delivery_evidence/fixtures/dirty/delivery/ui/evidence.yaml": "777b4f7921e302dfce79cd764bba8b44091ac9c7ee04a00f5dbb663342ec68a0",
670
+ "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/api/evidence.yaml": "2b848f160a401a217f56d53d87ce8e0a0efc0ed6d3781f6cc746e384c93eb8cc",
671
+ "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/data/evidence.yaml": "165dd67ff64f14c30268de5b18bf0c9c6634fb9ca48439c660c1962cebe3e0cb",
672
+ "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/orphan/README.md": "7d480b1fff6546615e55d866784bc2267d06d0221dfde8d0ba0353fa1ea0db9a",
673
+ "detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/ui/evidence.yaml": "777b4f7921e302dfce79cd764bba8b44091ac9c7ee04a00f5dbb663342ec68a0",
674
674
  "detectors/htmx_e2e_detector/atdd.implementation.yaml": "454bb123039d57c992d27d9274e20fa3452ac41a237eb55471be62a4bcfebf06",
675
675
  "detectors/htmx_e2e_detector/checks/_e2e.mjs": "4e3605500ac232603083790c3984de5c828028c60eef366b26aba18d38fce30b",
676
676
  "detectors/htmx_e2e_detector/checks/_map.json": "94dec08ff4764cd6a3dba139f12a7c984292e95a9354b0cdaa0ce9d4180e2c89",
@@ -1172,7 +1172,7 @@
1172
1172
  "planner-schemas/binding-lock.schema.json": "f2455606702f45fafa194549126d4a66e6c286ba1c4ba9e6bdcf932286a6d34d",
1173
1173
  "planner-schemas/component.schema.json": "d20d3502c155627a120cb10d5e9bdec4ba2c73af9c3e9a9d10e3c351f167c72a",
1174
1174
  "planner-schemas/contract-registry.schema.json": "b9dbaf6c87d4f4d676e3b4251344b9eada523b1c5e739c0423cabe267012c12e",
1175
- "planner-schemas/delivery-config.schema.json": "34e46c305b85340844086e60b2f5cbc1105ecc03ca63e5f9e494eff62353d30d",
1175
+ "planner-schemas/delivery-config.schema.json": "15b70a67c0725b441e6b9d428e9ccd76c06f4ee6d36406050893b70ecc130841",
1176
1176
  "planner-schemas/delivery-evidence.schema.json": "9b084eb73752d5637ba1d429f3709f8795cc2860c21c0b9bd58751b9887b001c",
1177
1177
  "planner-schemas/feature.schema.json": "eb4aa40d89067ae5c86a079bb3d8e4f45c8727f9e15db2b86c08dacf8e4ae56a",
1178
1178
  "planner-schemas/journey.schema.json": "1ff705bcf89a2823cf60c9e1597e254f97844e7a74dc58299d53192ad2c9f8c3",
@@ -1190,12 +1190,12 @@
1190
1190
  "src/agent.ts": "b2015873940ca83bb8fdc683dbb85c2d53fe6f8c3e2b6b0b71bda895f0c1eb2f",
1191
1191
  "src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
1192
1192
  "src/cli.ts": "bb2047cee60f48bfe3eaee4c1b3dcd3538a0e1edc170cf58e87686117bea07a7",
1193
- "src/delivery.ts": "7962144972ae1f853a25cd88cf0cb7d4bca247b1193cd5eb33acc1ef272072cb",
1194
- "src/docs-capability.ts": "0feae94b7697dd53d8a457b24765e81fcf804c774f0cc4b6035cb3da822e486a",
1193
+ "src/delivery.ts": "938af0efee874888b20d0f862cccd3a492dafa69352649a6a584972583df361c",
1194
+ "src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
1195
1195
  "src/enforce.ts": "58ad0593895912bdc1effe97430b07acf8cda066052397957558cbb331c16778",
1196
1196
  "src/hooks.ts": "adbb72f7a83f53d596fd217f01f4ce32c5a59a23ab81993fed226fb96fd1cbd1",
1197
1197
  "src/index.ts": "8ee4d7716794f6990ae8580e471b98256a0d0223dad063cd3b4542e598b6e744",
1198
- "src/integrity.ts": "1039d80976eb003a927f28bbcb7e220a02169ce2b0df460b4251bd00cfa3be8b",
1198
+ "src/integrity.ts": "7cbb1ad4991079a4852d2d131c0769f66e13dc8b5fa0e637780a1b70a75eaf78",
1199
1199
  "src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
1200
1200
  "src/planner-kernel.ts": "5d3f5305fb59becb2cc97d8a03aca6b104f385805d91cc0410856e289dd6fbb1",
1201
1201
  "src/planner-schema-validator.ts": "ce529c0936171075e93c7539b3655fe895a98dd2973ebda5775958e6ebc316d9",
@@ -1208,7 +1208,7 @@
1208
1208
  "templates/agents/AGENTS.block.md": "7e9687b1eff245b66da4127b336a9e20af2f5fa273e08895188c0a0dffebd77d",
1209
1209
  "templates/agents/atdd-bun.integrity.test.ts": "dec6f6e5f65a08d9512fc703b367c163f8c9da1f6aa7fd466835c1ec30348a41",
1210
1210
  "templates/agents/atdd/SKILL.md": "b621abe22851a75b30fc0e6c339a2c0a3ed6bbf78b8c17000a4ce301e05df96d",
1211
- "templates/agents/delivery/SKILL.md": "72c279fba45c867d0a5b5e02f34dcba89efe06dfc9230c7aad501d2ae3629c35",
1211
+ "templates/agents/delivery/SKILL.md": "aa9833d1ea4dbeb04a639f8a9dcd38803ae6ed10370da2d5298778c4636e25c9",
1212
1212
  "templates/agents/delivery/review.md": "289ee5d55ba0f321e5703a6fd8d5417d55bf0d9c6dc3103ea0f79af11c159d9c",
1213
1213
  "templates/github/atdd-bun-release.yml": "d9547e9e6ef3ae010d53d55314f6a54567f88bd900f7e50d994cd308601d2684",
1214
1214
  "templates/github/atdd-bun.yml": "bb42414f72f4b9a2fb1eb69530695e193f105a984542f4ca124c898a5f1489be"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
@@ -9,7 +9,7 @@
9
9
  "root": {
10
10
  "type": "string",
11
11
  "pattern": "^[a-z0-9_][a-z0-9_.-]*(?:/[a-z0-9_][a-z0-9_.-]*)*$",
12
- "description": "Repository-relative folder holding one <tranche>/evidence.yaml per tranche, in canonical form: no leading, trailing or doubled slash, no dot segments. Default: delivery. Changing it after adoption is a loosening the integrity check reports."
12
+ "description": "Repository-relative folder holding one <tranche>/evidence.yaml per tranche, in canonical form: no leading, trailing or doubled slash, no dot segments. Default: docs/delivery/tranches, beside the program's reasoning in the docs profile's delivery area. Changing it after adoption is a loosening the integrity check reports."
13
13
  },
14
14
  "require_record": {
15
15
  "type": "boolean",
package/src/delivery.ts CHANGED
@@ -39,6 +39,12 @@ const DEFAULT_STAGES: Record<Stage, Omit<StagePolicy, "independence">> = {
39
39
  code_review: { authors: ["glm", "claude"], reviewers: ["glm", "claude"] },
40
40
  final_review: { authors: ["codex"], reviewers: ["codex", "claude"] },
41
41
  };
42
+ /** Tranche records live with the program's reasoning, under the docs profile's delivery area; the docs profile leaves
43
+ * this folder to the delivery profile (records are YAML and data, never authored AsciiDoc). */
44
+ export const DEFAULT_ROOT = "docs/delivery/tranches";
45
+ /** The default before 0.9.0. */
46
+ const LEGACY_ROOT = "delivery";
47
+ const inDocs = (root: string) => root === "docs" || root.startsWith("docs/");
42
48
  const DEFAULT_FALLBACK: DeliveryPolicy["fallback"] = { after_failures: 3, within_minutes: 10, when_exhausted: "block" };
43
49
  const packageRoot = resolve(import.meta.dir, "..");
44
50
 
@@ -71,14 +77,14 @@ export function deliveryPolicy(block: unknown): DeliveryPolicy {
71
77
  }
72
78
  const fallback = record(raw.fallback) ?? {};
73
79
  return {
74
- root: canonicalRoot(text(raw.root, "delivery")), independence, stages,
80
+ root: canonicalRoot(text(raw.root, DEFAULT_ROOT)), independence, stages,
75
81
  fallback: { after_failures: count(fallback.after_failures, DEFAULT_FALLBACK.after_failures), within_minutes: count(fallback.within_minutes, DEFAULT_FALLBACK.within_minutes), when_exhausted: fallback.when_exhausted === "wait" ? "wait" : "block" },
76
82
  commands: (record(raw.commands) ?? {}) as DeliveryPolicy["commands"], require_record: raw.require_record === false ? false : true, multiplexer: text(raw.multiplexer, "herdr"),
77
83
  };
78
84
  }
79
85
 
80
86
  /** One spelling per root, so filesystem discovery, Git pathspecs and drift filtering agree ("delivery/" is "delivery"). */
81
- export const canonicalRoot = (root: string) => root.replaceAll("\\", "/").split("/").filter(part => part && part !== ".").join("/") || "delivery";
87
+ export const canonicalRoot = (root: string) => root.replaceAll("\\", "/").split("/").filter(part => part && part !== ".").join("/") || DEFAULT_ROOT;
82
88
 
83
89
  /** Why `current` enforces less than `base`, for the integrity check's loosening report. Tightening is silent. */
84
90
  export function loosenedDelivery(base: unknown, current: unknown): string[] {
@@ -102,7 +108,9 @@ export function loosenedDelivery(base: unknown, current: unknown): string[] {
102
108
  if (promoted.length) out.push(`delivery.stages.${stage}.${role} [${b[role].join(", ")}] → [${c[role].join(", ")}] promotes ${promoted.join(", ")}`);
103
109
  }
104
110
  }
105
- // Moving the root hides every earlier record from the validator and the gate.
111
+ // Moving the root hides every earlier record from the validator and the gate. No exception, not even pinning the 0.8.0
112
+ // default: from the config alone it cannot be told apart from moving a 0.9 repository's records out of view, so a
113
+ // human approves it.
106
114
  if (before.root !== after.root) out.push(`delivery.root ${before.root} → ${after.root}`);
107
115
  if (before.require_record && !after.require_record) out.push("delivery.require_record true → false");
108
116
  if (before.fallback.when_exhausted === "block" && after.fallback.when_exhausted === "wait") out.push("delivery.fallback.when_exhausted block → wait");
@@ -140,10 +148,44 @@ type Finding = { id: string; severity: string; rebuttal?: string; outcome?: "fix
140
148
  type Evidence = { tranche: string; status: "open" | "ready"; base_sha: string; approved_sha?: string; reviews: Review[] };
141
149
  export type EvidenceFile = { file: string; tranche: string; data: Evidence | null; error?: string };
142
150
 
151
+ /** Files in the records folder that are neither a tranche's evidence.yaml nor a data file (a report): authored documents
152
+ * or code would otherwise sit in a folder no other profile judges. Loose files directly under the root count too. */
153
+ export async function strayFiles(root: string, policy: DeliveryPolicy): Promise<string[]> {
154
+ const dir = join(root, policy.root), out: string[] = [];
155
+ if (!isFolder(dir)) return out;
156
+ const walk = async (path: string, depth: number): Promise<void> => {
157
+ for (const entry of await readdir(path, { withFileTypes: true })) {
158
+ const child = join(path, entry.name), rel = `${policy.root}/${child.slice(dir.length + 1).replaceAll("\\", "/")}`;
159
+ if (entry.isSymbolicLink() && depth === 0) continue; // a symlinked tranche folder is reported by loadEvidence
160
+ if (entry.isSymbolicLink()) { out.push(rel); continue; } // a symlinked file points outside what any rule judges
161
+ if (entry.isDirectory()) await walk(child, depth + 1);
162
+ else if (depth === 0 || !(entry.name === "evidence.yaml" && depth === 1 || REPORT_EXTENSION.test(entry.name))) out.push(rel);
163
+ }
164
+ };
165
+ await walk(dir, 0);
166
+ return out.sort();
167
+ }
168
+
169
+ /** Data files inside tranche folders (depth 1 and below, other than evidence.yaml): candidate reports. */
170
+ async function dataFiles(root: string, policy: DeliveryPolicy): Promise<string[]> {
171
+ const dir = join(root, policy.root), out: string[] = [];
172
+ if (!isFolder(dir)) return out;
173
+ const walk = async (path: string, depth: number): Promise<void> => {
174
+ for (const entry of await readdir(path, { withFileTypes: true })) {
175
+ const child = join(path, entry.name);
176
+ if (entry.isSymbolicLink()) continue;
177
+ if (entry.isDirectory()) await walk(child, depth + 1);
178
+ else if (depth >= 1 && !(entry.name === "evidence.yaml" && depth === 1) && REPORT_EXTENSION.test(entry.name)) out.push(`${policy.root}/${child.slice(dir.length + 1).replaceAll("\\", "/")}`);
179
+ }
180
+ };
181
+ await walk(dir, 0);
182
+ return out;
183
+ }
184
+
143
185
  /** Every tranche folder under the delivery root with its parsed evidence, or why it has none. */
144
186
  export async function loadEvidence(root: string, policy: DeliveryPolicy): Promise<EvidenceFile[]> {
145
187
  const dir = join(root, policy.root);
146
- if (!existsSync(dir)) return [];
188
+ if (!isFolder(dir)) return [];
147
189
  const out: EvidenceFile[] = [];
148
190
  for (const entry of (await readdir(dir, { withFileTypes: true })).filter(entry => entry.isDirectory() || entry.isSymbolicLink()).sort((a, b) => a.name.localeCompare(b.name))) {
149
191
  // A symlinked tranche folder could point anywhere, and would otherwise be skipped unread.
@@ -181,6 +223,13 @@ function checkModels(file: string, review: Review, policy: StagePolicy, at: stri
181
223
 
182
224
  /** Reports are data, never code: a report path is exempt from drift, so it must not be able to name a source file. */
183
225
  const REPORT_EXTENSION = /\.(json|jsonl|yaml|yml|txt|md|log)$/;
226
+ /** The reports a record names, read defensively: this runs before schema validation, so a malformed record (reviews not
227
+ * a list, an entry not a mapping, a report not a string) names nothing and is left to the schema rule to report. */
228
+ const namedReports = (data: unknown): string[] => {
229
+ const reviews = record(data)?.reviews;
230
+ return Array.isArray(reviews) ? reviews.flatMap(review => { const report = record(review)?.report; return typeof report === "string" && report ? [report] : []; }) : [];
231
+ };
232
+ const isFolder = (path: string) => { try { return lstatSync(path).isDirectory(); } catch { return false; } };
184
233
  const regularFile = (path: string) => { try { return lstatSync(path).isFile(); } catch { return false; } };
185
234
 
186
235
  /** Two spellings of one commit: an abbreviated SHA is a prefix of the full one. */
@@ -296,7 +345,22 @@ export async function validateDelivery(root = process.cwd(), options: DeliveryOp
296
345
  const overlap = owned.find(other => policy.root === other || policy.root.startsWith(`${other}/`) || other.startsWith(`${policy.root}/`));
297
346
  if (overlap) {
298
347
  findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `delivery.root ${policy.root} overlaps the ${overlap} root; the delivery root holds only records and reports`));
299
- policy = { ...policy, root: "delivery" };
348
+ policy = { ...policy, root: DEFAULT_ROOT };
349
+ } else if (inDocs(policy.root) && policy.root !== DEFAULT_ROOT) {
350
+ // The docs profile gives up exactly one folder under docs/; any other root there would take authored documentation
351
+ // out of its rules.
352
+ findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `delivery.root ${policy.root} is inside docs/; the only delivery root there is ${DEFAULT_ROOT}, the one folder the docs profile leaves to delivery`));
353
+ policy = { ...policy, root: DEFAULT_ROOT };
354
+ }
355
+ // 0.8.0 kept records under delivery/ by default. Records left there under any other effective root would be unseen.
356
+ if (policy.root !== LEGACY_ROOT && isFolder(join(absolute, LEGACY_ROOT))) {
357
+ const legacy = (await readdir(join(absolute, LEGACY_ROOT), { withFileTypes: true })).filter(entry => entry.isDirectory() && existsSync(join(absolute, LEGACY_ROOT, entry.name, "evidence.yaml")));
358
+ if (legacy.length) findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `tranche records under delivery/ (${legacy.map(entry => entry.name).join(", ")}) are outside the root ${policy.root}; 0.8.0 kept them there by default; set delivery.root: delivery (a root change the integrity check reports for a human to approve once), or move them there, which rewrites merged records and so needs a human-supervised merge`));
359
+ }
360
+ // The other direction: records under the default root while another root is configured are outside what is judged.
361
+ if (policy.root !== DEFAULT_ROOT && isFolder(join(absolute, DEFAULT_ROOT))) {
362
+ const hidden = (await readdir(join(absolute, DEFAULT_ROOT), { withFileTypes: true })).filter(entry => entry.isDirectory() && existsSync(join(absolute, DEFAULT_ROOT, entry.name, "evidence.yaml")));
363
+ if (hidden.length) findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `tranche records under ${DEFAULT_ROOT} (${hidden.map(entry => entry.name).join(", ")}) are outside the configured root ${policy.root}; move them there, or remove delivery.root`));
300
364
  }
301
365
  const validEvidence = await schema("delivery-evidence.schema.json"), files = await loadEvidence(absolute, policy);
302
366
  const mode = options.gate === undefined ? gateMode() : options.gate === true ? "merge" : options.gate || null;
@@ -305,6 +369,13 @@ export async function validateDelivery(root = process.cwd(), options: DeliveryOp
305
369
  // fail every later change. Outside the gate every record is judged. The gate itself still rejects any change to
306
370
  // an untouched record's folder (mergeGate).
307
371
  const onBase = mode ? (await gateRange(absolute, mode, options.base))?.base ?? null : null;
372
+ // At the gate, like records, a stray the change does not touch was there when it merged and is not judged again.
373
+ // A data file belongs only as a report some record in its tranche names; an unnamed one is authored content by another name.
374
+ // Per tranche: a data file is a report only when a record in its own tranche names it.
375
+ const reported = new Set(files.flatMap(entry => namedReports(entry.data).filter(report => report.startsWith(`${policy.root}/${entry.tranche}/`))));
376
+ const unnamed = (await dataFiles(absolute, policy)).filter(path => !reported.has(path));
377
+ if (existsSync(join(absolute, policy.root)) && !isFolder(join(absolute, policy.root))) findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `delivery.root ${policy.root} is a file, not a folder; the records cannot be read`));
378
+ for (const path of [...await strayFiles(absolute, policy), ...unnamed].sort()) if (!onBase || (await git(absolute, ["diff", "--quiet", onBase, "--", path])).code) findings.push(finding("delivery.evidence-schema", path, `${path} is neither a tranche's evidence.yaml nor a report a record in its tranche names (a data file: ${REPORT_EXTENSION.source.slice(3, -2).replaceAll("|", ", ")}); the records folder holds only records and their reports`));
308
379
  for (const entry of files) {
309
380
  if (onBase && existsSync(join(absolute, entry.file)) && !(await git(absolute, ["diff", "--quiet", onBase, "--", `${policy.root}/${entry.tranche}`])).code) continue;
310
381
  if (!entry.data) { findings.push(finding("delivery.evidence-schema", entry.file, entry.error ?? "evidence is missing")); continue; }
@@ -376,9 +447,16 @@ async function mergeGate(root: string, policy: DeliveryPolicy, files: EvidenceFi
376
447
  // A record or report already on the base branch is final: editing an old record would make its reports "named by a
377
448
  // changed record" and so exempt from drift. A later change to a tranche is a new tranche with its own record.
378
449
  const final = (await git(root, ["diff", "--no-renames", "--relative", "--name-only", "--diff-filter=MRTC", ...span, "--", policy.root])).out.split("\n").filter(Boolean);
450
+ // Records 0.8.0 kept under delivery/ stay append-only after the root moves: deleting or editing one is reported even
451
+ // though it is outside the current root. Only exact <tranche>/evidence.yaml paths, so an unrelated delivery/ folder
452
+ // in the project is not touched.
453
+ if (policy.root !== LEGACY_ROOT) {
454
+ const legacy = (await git(root, ["diff", "--no-renames", "--relative", "--name-only", "--diff-filter=DMRTC", ...span, "--", LEGACY_ROOT])).out.split("\n").filter(path => /^delivery\/[^/]+\/evidence\.yaml$/.test(path));
455
+ for (const path of legacy) out.push(finding("delivery.merge-gate", path, `${mode === "merge" ? "the branch" : "this push"} ${existsSync(join(root, path)) ? "modifies" : "deletes"} ${path}, a record 0.8.0 kept under delivery/; records stay append-only when the root moves`));
456
+ }
379
457
  for (const path of final) out.push(finding("delivery.merge-gate", path, `${mode === "merge" ? "the branch" : "this push"} modifies ${path}, which is already on the base branch; merged records and reports are final, so a later change needs a new tranche`));
380
458
  // Under the root, only records and the reports a changed record names may change: anything else is unbound.
381
- const named = new Set(changed.flatMap(path => files.find(file => file.file === path)?.data?.reviews?.flatMap(review => review.report ? [review.report] : []) ?? []));
459
+ const named = new Set(changed.flatMap(path => namedReports(files.find(file => file.file === path)?.data)));
382
460
  for (const path of changedHere.filter(path => path.startsWith(`${policy.root}/`) && !isRecord(path) && !named.has(path)))
383
461
  out.push(finding("delivery.merge-gate", path, `${mode === "merge" ? "the branch" : "this push"} changes ${path} under ${policy.root}/, and no record it changes names it as a report; only <tranche>/evidence.yaml records and their reports live there`));
384
462
  if (policy.require_record && outside.length && !changed.length) out.push(finding("delivery.merge-gate", "atdd-bun.yaml", `${mode === "merge" ? "the branch" : "this push"} changes ${outside.slice(0, 5).join(", ")}${outside.length > 5 ? ` and ${outside.length - 5} more` : ""} with no tranche record under ${policy.root}/; every change merges through a reviewed tranche (delivery.require_record)`));
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { journeyDocs, journeyDocsApply } from "./journey-docs";
3
+ import { DEFAULT_ROOT, deliveryAdopted, deliveryPolicy } from "./delivery";
3
4
  import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
4
5
  import { tmpdir } from "node:os";
5
6
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
@@ -50,6 +51,23 @@ export function parseAttributes(text: string): { attrs: Record<string, string>;
50
51
  return { attrs, lines };
51
52
  }
52
53
 
54
+ /** The delivery profile's records folder, when it is adopted: tranche records and reports are YAML and data, judged by
55
+ * that profile, never authored documentation. The docs profile leaves the folder (by default docs/delivery/tranches) to
56
+ * it; the rest of docs/delivery, the program's reasoning, stays documentation. */
57
+ async function deliveryRecords(root: string): Promise<(path: string) => boolean> {
58
+ const file = join(root, "atdd-bun.yaml");
59
+ try {
60
+ const data = existsSync(file) ? Bun.YAML.parse(await readFile(file, "utf8")) as Record<string, unknown> | null : null;
61
+ if (!deliveryAdopted(data)) return () => false;
62
+ // Only the one folder the delivery profile may use under docs/; a root configured anywhere else in docs/ is a delivery
63
+ // finding and exempts nothing here.
64
+ if (deliveryPolicy(data!.delivery).root !== DEFAULT_ROOT) return () => false;
65
+ // Records and data only: AsciiDoc there stays documentation, judged by every docs rule (the delivery profile also
66
+ // reports it as a file that does not belong in the records folder).
67
+ return path => path.startsWith(`${DEFAULT_ROOT}/`) && !path.endsWith(".adoc");
68
+ } catch { return () => false; }
69
+ }
70
+
53
71
  async function documents(root: string): Promise<Document[]> {
54
72
  const paths = await walk(join(root, DOCS), ".adoc");
55
73
  return Promise.all(paths.filter(path => !generated(relative(root, path).replaceAll("\\", "/"))).map(async path => {
@@ -87,8 +105,8 @@ function adrViolations(docs: Document[]): DocumentationViolation[] {
87
105
  }
88
106
 
89
107
  export async function scanDocumentation(root: string): Promise<DocumentationViolation[]> {
90
- const absolute = resolve(root), docs = await documents(absolute), output: DocumentationViolation[] = [];
91
- for (const path of await walk(join(absolute, DOCS), ".md")) { const file = relative(absolute, path).replaceAll("\\", "/"); if (!generated(file)) output.push(violation("planner.docs.asciidoc-only", file, 1, `authored markdown beneath docs/: ${file}. AsciiDoc is the only authored format; convert it, and convert a historical document INTO docs/archive/ rather than into a current area.`, lineAt(await readFile(path, "utf8"), 1))); }
108
+ const absolute = resolve(root), docs = await documents(absolute), output: DocumentationViolation[] = [], records = await deliveryRecords(absolute);
109
+ for (const path of await walk(join(absolute, DOCS), ".md")) { const file = relative(absolute, path).replaceAll("\\", "/"); if (!generated(file) && !records(file)) output.push(violation("planner.docs.asciidoc-only", file, 1, `authored markdown beneath docs/: ${file}. AsciiDoc is the only authored format; convert it, and convert a historical document INTO docs/archive/ rather than into a current area.`, lineAt(await readFile(path, "utf8"), 1))); }
92
110
  for (const doc of docs) { const missing = ["doc-id", "status"].filter(name => !doc.attrs[name]); if (missing.length) output.push(violation("planner.docs.identity-required", doc.path, 1, `document declares no ${missing.map(name => `:${name}:`).join(" and no ")}. Identity and currency are both required: an id with no status is a node whose currency is unknown, a status with no id is a claim nothing can reference.`, lineAt(doc.text, 1))); }
93
111
  const byId = new Map<string, Document[]>(); for (const doc of docs) if (doc.attrs["doc-id"]) byId.set(doc.attrs["doc-id"], [...(byId.get(doc.attrs["doc-id"]) ?? []), doc]);
94
112
  for (const [id, group] of byId) if (group.length > 1) for (const doc of group) { const line = doc.lines["doc-id"] ?? 1; output.push(violation("planner.docs.doc-id-unique", doc.path, line, `doc-id ${JSON.stringify(id)} is declared by ${group.length} documents: ${group.map(d => d.path).join(", ")}. Resolution needs exactly one target per id.`, lineAt(doc.text, line))); }
@@ -106,12 +124,12 @@ async function journeyViewViolations(root: string): Promise<DocumentationViolati
106
124
  return result.stale.map(file => violation("planner.docs.journey-view-current", file, 1, `${file} does not match what plan/ generates, so the journey documentation no longer shows the plan. Regenerate it with \`atdd-bun docs journeys\` and commit the result; never edit it by hand.`));
107
125
  }
108
126
 
109
- export function declarationViolations(declaration: DocumentationDeclaration | null, changeSet?: string[]): DocumentationViolation[] {
127
+ export function declarationViolations(declaration: DocumentationDeclaration | null, changeSet?: string[], records: (path: string) => boolean = () => false): DocumentationViolation[] {
110
128
  if (!declaration) return [];
111
129
  const artifacts = Array.isArray(declaration.artifacts) ? declaration.artifacts : []; const output: DocumentationViolation[] = [];
112
130
  if (declaration.impact !== "change" && declaration.impact !== "none") output.push(violation("planner.docs.artifact-path-shape", "<declaration>", 1, `declaration carries impact=${JSON.stringify(declaration.impact)}; the two total forms are ["change", "none"]. A malformed declaration is reported, never treated as nothing-to-check.`));
113
131
  for (const [index, artifact] of artifacts.entries()) { const path = artifact.path ?? "", problems: string[] = []; if (!path) problems.push("declares no path"); else { if (!path.startsWith("docs/")) problems.push(`path ${JSON.stringify(path)} is outside the canonical tree (must begin "docs/")`); if (!path.endsWith(".adoc")) problems.push(`path ${JSON.stringify(path)} is not AsciiDoc (must end ".adoc")`); if (artifact.action === "archive" && !path.startsWith("docs/archive/")) problems.push(`archive destination ${JSON.stringify(path)} is outside "docs/archive/" — archiving must preserve history, never promote it into a current area`); } if (problems.length) output.push(violation("planner.docs.artifact-path-shape", path || "<declaration>", 1, `declared artifact[${index}] (action: ${artifact.action || "unset"}): ${problems.join("; ")}`)); }
114
- if (changeSet) { const covered = new Set(artifacts.flatMap(a => [a.path, a.from]).filter((p): p is string => Boolean(p))); for (const path of [...new Set(changeSet)].sort()) if (path.startsWith("docs/") && !generated(path) && !covered.has(path)) output.push(violation("planner.docs.undeclared-change", path, 1, `the change set touches ${path} and no declared artifact covers it. Declare it at RATIFY; if the change was not planned, the declaration was wrong at RATIFY and re-ratifying is the honest correction.`)); }
132
+ if (changeSet) { const covered = new Set(artifacts.flatMap(a => [a.path, a.from]).filter((p): p is string => Boolean(p))); for (const path of [...new Set(changeSet)].sort()) if (path.startsWith("docs/") && !generated(path) && !records(path) && !covered.has(path)) output.push(violation("planner.docs.undeclared-change", path, 1, `the change set touches ${path} and no declared artifact covers it. Declare it at RATIFY; if the change was not planned, the declaration was wrong at RATIFY and re-ratifying is the honest correction.`)); }
115
133
  return output;
116
134
  }
117
135
 
@@ -152,7 +170,7 @@ export async function renderDocumentation(root: string, timeoutMs = 120_000): Pr
152
170
  async function checkDocumentationInner(input: { root: string; declaration: DocumentationDeclaration | null; changeSet: string[] | null; render?: () => Promise<DocumentationRender> }): Promise<DocumentationCheck> {
153
171
  if (input.declaration?.impact === "none") return { verdict: "NOT_APPLICABLE", findings: [], checked: [] };
154
172
  const corpus = await scanDocumentation(input.root); const findings: DocumentationCheck["findings"] = [...corpus]; const checked = (await documents(resolve(input.root))).map(d => d.path); let definite = corpus.length > 0;
155
- if (input.declaration) { const declarationFindings = declarationViolations(input.declaration, input.changeSet ?? undefined); findings.push(...declarationFindings); definite ||= declarationFindings.length > 0; checked.push("<declaration>"); if (input.declaration.impact === "change") for (const artifact of input.declaration.artifacts ?? []) if (["create", "modify"].includes(artifact.action ?? "") && artifact.path && !existsSync(join(input.root, artifact.path))) { findings.push(violation("planner.docs.artifact-path-shape", artifact.path, 1, `declared artifact ${JSON.stringify(artifact.path)} (action: ${artifact.action}) is not in the tree, so it was never examined; a declared document that was never written has not discharged the obligation.`)); definite = true; } } else findings.push({ rule_id: null, where: "<declaration>", message: "core supplied no documentation declaration, so no declaration-dependent rule could be evaluated. This is COULD_NOT_CHECK and it BLOCKS." });
173
+ if (input.declaration) { const declarationFindings = declarationViolations(input.declaration, input.changeSet ?? undefined, await deliveryRecords(resolve(input.root))); findings.push(...declarationFindings); definite ||= declarationFindings.length > 0; checked.push("<declaration>"); if (input.declaration.impact === "change") for (const artifact of input.declaration.artifacts ?? []) if (["create", "modify"].includes(artifact.action ?? "") && artifact.path && !existsSync(join(input.root, artifact.path))) { findings.push(violation("planner.docs.artifact-path-shape", artifact.path, 1, `declared artifact ${JSON.stringify(artifact.path)} (action: ${artifact.action}) is not in the tree, so it was never examined; a declared document that was never written has not discharged the obligation.`)); definite = true; } } else findings.push({ rule_id: null, where: "<declaration>", message: "core supplied no documentation declaration, so no declaration-dependent rule could be evaluated. This is COULD_NOT_CHECK and it BLOCKS." });
156
174
  if (input.declaration && input.changeSet === null) findings.push({ rule_id: null, where: "<change_set>", message: "core supplied no change set, so whether this diff touches docs/ without declaring it could not be established. This is COULD_NOT_CHECK and it BLOCKS." });
157
175
  const rendered = await (input.render ?? (() => renderDocumentation(input.root)))(); findings.push(...rendered.findings); definite ||= rendered.findings.length > 0; if (rendered.couldNotCheck) findings.push({ rule_id: "planner.docs.reference-integrity", file: "docs/", line: 1, col: 1, evidence: rendered.couldNotCheck, source_line: "" }); else checked.push("<render:asciidoctor>");
158
176
  // Fail closed on any rule id this capability does not declare, however it was built (a renderer can return anything).
package/src/integrity.ts CHANGED
@@ -168,7 +168,17 @@ async function checkPolicy(root: string, base?: string, push = process.env.GITHU
168
168
  if (!against) return [];
169
169
  const read = async (text: string | null) => (text ? Bun.YAML.parse(text) ?? {} : {}) as Partial<HookPolicy>;
170
170
  const before = await git(root, ["show", `${against}:atdd-bun.yaml`]), path = join(root, "atdd-bun.yaml");
171
- const loosened = loosenedPolicy(await read(before.code ? null : before.out), await read(existsSync(path) ? await readFile(path, "utf8") : null));
171
+ // A baseline that does not parse, or parses to something other than a policy mapping, cannot be compared; reading it as
172
+ // empty would compare against the defaults and could miss a loosening the baseline configured. It is a finding, as an
173
+ // unreadable working-tree file is.
174
+ const unreadable = (why: string) => [{ file: "atdd-bun.yaml", detail: `the baseline atdd-bun.yaml at ${against.slice(0, 7)} ${why}, so the policy cannot be compared`, restore: push
175
+ ? `this push is compared with the tip it replaced (${against.slice(0, 7)}), whose atdd-bun.yaml is broken; once the repaired atdd-bun.yaml is on the branch, the next push is compared with a readable tip`
176
+ : `repair the atdd-bun.yaml on the base branch (git show ${against.slice(0, 7)}:atdd-bun.yaml) and land it there, which may need a maintainer; then bring that repair into this branch (rebase onto the base, or merge it in: a pull request is compared with its merge base) and re-run the check` }];
177
+ let baseline: Partial<HookPolicy>;
178
+ try { baseline = await read(before.code ? null : before.out); }
179
+ catch (error) { return unreadable(`could not be parsed (${String(error)})`); }
180
+ if (typeof baseline !== "object" || baseline === null || Array.isArray(baseline)) return unreadable(`is not a policy mapping (${JSON.stringify(baseline)})`);
181
+ const loosened = loosenedPolicy(baseline, await read(existsSync(path) ? await readFile(path, "utf8") : null));
172
182
  return loosened.length ? [{ file: "atdd-bun.yaml", detail: `loosens the policy of ${against.slice(0, 7)}: ${loosened.join("; ")}`, restore: `git checkout ${against.slice(0, 7)} -- atdd-bun.yaml` }] : [];
173
183
  }
174
184
 
@@ -12,7 +12,7 @@ A **tranche** is one independently mergeable piece of the program, on its own br
12
12
 
13
13
  Its job is throughput: every worker slot busy, every tranche moving. It never implements, repairs tests, reviews, or merges a tranche.
14
14
 
15
- 1. Split the program into tranches with explicit dependencies. Activate a tranche as soon as its own dependencies have merged; do not wait for a whole wave. A tranche whose dependencies are still open may run PLAN and `plan_review` but nothing after; revalidate its plan once they merge.
15
+ 1. Split the program into tranches with explicit dependencies, and write why the program exists, its scope and how it was split in `docs/delivery/index.adoc` (a docs-profile document). Activate a tranche as soon as its own dependencies have merged; do not wait for a whole wave. A tranche whose dependencies are still open may run PLAN and `plan_review` but nothing after; revalidate its plan once they merge.
16
16
  2. For each active tranche, create a worktree from the owning repository's workspace and start a driver in it through the multiplexer (see Multiplexer). Send the mandate, submit it, wait 4–6 s, and read the pane: a working indicator or agent output means it landed; an empty prompt or placeholder means retry before waiting on anything.
17
17
  3. Wait on the multiplexer's events, not polling loops, and on every driver at once. When a slot frees, give it to the next ready tranche or to planning ahead.
18
18
  4. Keep provider health for the whole program. When a driver reports a model unavailable, tell every driver to go straight to the next model in its lists until it recovers, so no tranche spends time rediscovering an outage.
@@ -40,11 +40,11 @@ Agents run in panes of the terminal multiplexer named in `delivery.multiplexer`
40
40
  3. Fallback: after `fallback.after_failures` failures within `fallback.within_minutes` (outage, rate limit, no auditable report), use the next model in the stage's list and record it with its `kind` (`outage`, `rate_limit`, `no_report`, `timeout`), `failures`, and the `window` from the first to the last counted failure. REQUEST CHANGES is never a failure. With the list exhausted, `when_exhausted: block` emits `BLOCKED provider-unavailable`; `wait` keeps retrying the last model.
41
41
  4. For each finding of a REQUEST CHANGES review, either have the author fix it, or write one rebuttal with evidence (a test result, a rule id, file:line). Then run a fresh review of the same stage. If that reviewer upholds a disputed finding, emit `BLOCKED disputed-finding`; never dispute it a second time.
42
42
  5. Any commit, regenerated file, conflict fix or rebase after an approval cancels it. A change to the code goes back through `code_review`, then `final_review`.
43
- 6. Append every review to `<root>/<tranche>/evidence.yaml` as it happens; never edit an earlier entry, only add each finding's `outcome` (`fixed`, `withdrawn`, or `human` with the `decision`). Keep the raw reviewer output inside the tranche's folder and name it in `report`; nothing else goes in that folder. When `final_review` approves, set `status: ready` and `approved_sha` to that SHA, commit the evidence and reports alone, push, and merge with a merge commit once CI is green. A squash or rebase merge writes a commit no reviewer saw, and CI fails it after the merge.
43
+ 6. Append every review to `<root>/<tranche>/evidence.yaml` (`delivery.root`, default `docs/delivery/tranches`) as it happens; never edit an earlier entry, only add each finding's `outcome` (`fixed`, `withdrawn`, or `human` with the `decision`). Keep the raw reviewer output inside the tranche's folder and name it in `report`; nothing else goes in that folder. When `final_review` approves, set `status: ready` and `approved_sha` to that SHA, commit the evidence and reports alone, push, and merge with a merge commit once CI is green. A squash or rebase merge writes a commit no reviewer saw, and CI fails it after the merge.
44
44
  7. Emit events the coordinator can wait on, one line each: `PROGRAM_EVENT <tranche> <PLAN|RED|COMMIT <sha>|WORKER_START <role> <model> <sha>|WORKER_END <role> <model> <verdict>|FALLBACK <role> <from>→<to> <reason>|PLAN_REVIEW <sha>|TEST_REVIEW <sha>|CODE_REVIEW <sha>|FINAL_REVIEW <sha>|PR_OPENED <url>|MERGED <sha>|BLOCKED <reason>|HEARTBEAT>`.
45
45
 
46
46
  ```yaml
47
- # <root>/<tranche>/evidence.yaml
47
+ # docs/delivery/tranches/<tranche>/evidence.yaml
48
48
  tranche: api
49
49
  status: open # ready once final_review approves
50
50
  base_sha: 3f2a91c
@@ -59,7 +59,7 @@ reviews:
59
59
  checked: [ACC-API-001, src/wagons/api, coder.bun.error-response-*]
60
60
  findings:
61
61
  - { id: F1, severity: high, evidence: "src/wagons/api/handler.ts:42", invariant: "coded error bodies", affects: [ui], proposed_fix: "return { code: 'API_NOT_FOUND' }" } # outcome added once a fresh code_review confirms the fix
62
- report: delivery/api/code_review-1.json
62
+ report: docs/delivery/tranches/api/code_review-1.json
63
63
  ```
64
64
 
65
65
  ## Default commands