@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 +15 -4
- package/conventions/delivery/delivery.config-schema.convention.yaml +2 -2
- package/conventions/delivery/delivery.evidence-schema.convention.yaml +2 -2
- package/conventions/delivery/delivery.merge-gate.convention.yaml +1 -1
- package/conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml +1 -0
- package/conventions/planner.docs/planner.docs.undeclared-change.convention.yaml +1 -0
- package/integrity.json +17 -17
- package/package.json +1 -1
- package/planner-schemas/delivery-config.schema.json +1 -1
- package/src/delivery.ts +84 -6
- package/src/docs-capability.ts +23 -5
- package/src/integrity.ts +11 -1
- package/templates/agents/delivery/SKILL.md +4 -4
- /package/detectors/delivery_evidence/fixtures/clean/{delivery → docs/delivery/tranches}/api/evidence.yaml +0 -0
- /package/detectors/delivery_evidence/fixtures/dirty/{delivery → docs/delivery/tranches}/api/evidence.yaml +0 -0
- /package/detectors/delivery_evidence/fixtures/dirty/{delivery → docs/delivery/tranches}/data/evidence.yaml +0 -0
- /package/detectors/delivery_evidence/fixtures/dirty/{delivery → docs/delivery/tranches}/orphan/README.md +0 -0
- /package/detectors/delivery_evidence/fixtures/dirty/{delivery → docs/delivery/tranches}/ui/evidence.yaml +0 -0
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.
|
|
119
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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.
|
|
2
|
+
"version": "0.9.0",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
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": "
|
|
96
|
-
"conventions/delivery/delivery.evidence-schema.convention.yaml": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
1194
|
-
"src/docs-capability.ts": "
|
|
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": "
|
|
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": "
|
|
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
|
@@ -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,
|
|
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("/") ||
|
|
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 (!
|
|
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:
|
|
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
|
|
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)`));
|
package/src/docs-capability.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|