@afokapu/atdd-bun 0.10.2 → 0.10.4

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
@@ -160,6 +160,13 @@ From 0.10.2 the pre-commit hook no longer counts uncommitted work: it refused ex
160
160
  commits that reduce it. `max_uncommitted_files` is no longer read; commit size stays limited by
161
161
  `max_staged_files` and `max_staged_changed_lines`.
162
162
 
163
+ From 0.10.3 each ready record is judged by the delivery policy in `atdd-bun.yaml` at its `approved_sha`:
164
+ a later policy change (dropping or reordering a model, removing a review) never makes a merged record
165
+ fail. Records in progress are judged by the current policy.
166
+
167
+ From 0.10.4 a stage with nothing written (a plan-only or no-op tranche) records no work; a review
168
+ whose range recorded none answers for the latest recorded work before it.
169
+
163
170
  Every key is optional; these are the defaults:
164
171
 
165
172
  ```yaml
@@ -11,7 +11,7 @@ terms:
11
11
  the identity of one process: a session id, a pane id with its start time, or the retained report. Two entries with the same run were produced by the same process.
12
12
  - term_id: independence
13
13
  text: >-
14
- fresh-process: the reviewer is a new process that wrote nothing. different-model: additionally, its model wrote none of the work it reviews; the final review answers for red, green and refactor when none of them is reviewed.
14
+ fresh-process: the reviewer is a new process that wrote nothing. different-model: additionally, its model wrote none of the work it reviews; the final review answers for red, green and refactor when none of them is reviewed, and a review whose range recorded no work answers for the latest recorded work before it.
15
15
  content:
16
16
  summary: >-
17
17
  A reviewer that edits becomes a writer, and a reviewer that already saw an earlier round is anchored to it. Independence is what makes an approval evidence.
@@ -4,7 +4,7 @@ kind: rule
4
4
  status: active
5
5
  name: A ready record has every configured stage approved
6
6
  statement: >-
7
- A record with status ready has, for every stage the policy gives a writer, recorded work, and for every stage it gives a reviewer, a last review that approves; its approved_sha is the SHA the last review of the closing stage approved; and every review names its retained raw report, which exists (REQUIRED).
7
+ A record with status ready has, for every stage the policy gives a reviewer, a last review that approves (a stage with nothing written records no work); its approved_sha is the SHA the last review of the closing stage approved; and every review names its retained raw report, which exists (REQUIRED).
8
8
  terms:
9
9
  - term_id: closing_stage
10
10
  text: >-
@@ -17,6 +17,8 @@ content:
17
17
  fix_hint: |
18
18
  Run the missing review, or set status back to open until it has run. Set approved_sha to the SHA the final review approved.
19
19
  exceptions:
20
+ - >-
21
+ A ready record is judged by the policy in atdd-bun.yaml at its approved_sha, the policy it was approved under; a later policy change does not re-judge it. A record in progress is judged by the current policy.
20
22
  - >-
21
23
  A stage the policy omits is not required. Omitting one loosens the policy and is reported by the integrity check until a human approves it.
22
24
  metadata:
package/integrity.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.10.2",
2
+ "version": "0.10.4",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
6
- "README.md": "763767126dd67f0f0ed0a3955cc6c707370f79b60739cab05773f41b1d37fa97",
6
+ "README.md": "afff8ee1ca16773915650f8ed3c1e58bbd839259a7963d16441a0259423c8756",
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",
@@ -100,8 +100,8 @@
100
100
  "conventions/delivery/delivery.model-allowed.convention.yaml": "1071a727f61bacd21d70921885826974234ef0a58c0a814454b9375a0432b0fe",
101
101
  "conventions/delivery/delivery.operating-model.convention.yaml": "980858b735974093c98e850d8f6dd83cb21eea86a8b6e8249de678f0fc336f1b",
102
102
  "conventions/delivery/delivery.review.convention.yaml": "d71b889a92ca695bc7bc989636ef16272860a4d3389a2f1e2cf3776257e97af8",
103
- "conventions/delivery/delivery.reviewer-independent.convention.yaml": "264df63e768a2a17dca782dddbba6ba47a21fa3dd9cd39ed150f9a00dd04bdb0",
104
- "conventions/delivery/delivery.stages-complete.convention.yaml": "2658cb9f4a4cd8939231e4ebb0ed4db2d0982c94690d25e4577de9dad4c2f742",
103
+ "conventions/delivery/delivery.reviewer-independent.convention.yaml": "8c436d66ef5b1f6d6bc5dd55a235caca2490020e88665eecadb2ca41cd4149c5",
104
+ "conventions/delivery/delivery.stages-complete.convention.yaml": "0e5dd8057ad51eff7906e3424a9686a81be1814447afb9a30acab43d39e49b4c",
105
105
  "conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml": "0ea4232beba90280c664f93d097cafa44c67c2919c1468d4823fdc9e686aa5d2",
106
106
  "conventions/planner.docs/planner.docs.area-index-required.convention.yaml": "fd931e9f225bfbb5e78436d0d8a2a89cc0f5114493579962c72dd971301becce",
107
107
  "conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml": "a9f6178d29abe9385342a46a2c57470a3631aaf5e26f01e3660b7088b042418e",
@@ -1195,7 +1195,7 @@
1195
1195
  "src/board.ts": "f04c425f74edc003609baec7b5b2d75c1bc98b0e177d702de865dd42d9dd95e5",
1196
1196
  "src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
1197
1197
  "src/cli.ts": "b1618d7a7b3b0d42238cb1160aea4ee7962fef8135a7c75b89b0f3fa30dc1f45",
1198
- "src/delivery.ts": "f78cb15e1eabeca1492bd8f57b2029370d9c2dbc3d3fb7d25ed3a18ea9de87cb",
1198
+ "src/delivery.ts": "3fd36b6f7dbe9b5682241cff16362618ac0a85c0ac93a238141ebabdbe402e52",
1199
1199
  "src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
1200
1200
  "src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
1201
1201
  "src/hooks.ts": "9779eb48f9d0cb6234c33f6616f98dde6583d2ee03ac3c19f3ace2d156af26d0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
package/src/delivery.ts CHANGED
@@ -145,6 +145,18 @@ export function loosenedDelivery(base: unknown, current: unknown): string[] {
145
145
  return out;
146
146
  }
147
147
 
148
+ /** The delivery policy a ready record was approved under: atdd-bun.yaml at its approved_sha. A merged record proves it met
149
+ * the policy of its time, so a later policy change never makes it fail. When that commit has no readable, adopting
150
+ * config, the current policy applies. The root stays today's: it only locates the records. */
151
+ async function policyAt(root: string, sha: string, current: DeliveryPolicy): Promise<DeliveryPolicy> {
152
+ const prefix = (await git(root, ["rev-parse", "--show-prefix"])).out, shown = await git(root, ["show", `${sha}:${prefix}atdd-bun.yaml`]);
153
+ if (shown.code) return current;
154
+ try {
155
+ const data = record(Bun.YAML.parse(shown.out));
156
+ return data && deliveryAdopted(data) ? { ...deliveryPolicy(data.delivery ?? {}), root: current.root } : current;
157
+ } catch { return current; }
158
+ }
159
+
148
160
  async function readConfig(root: string): Promise<{ data: Record<string, unknown> | null; error?: string }> {
149
161
  const file = join(root, "atdd-bun.yaml");
150
162
  if (!existsSync(file)) return { data: null };
@@ -246,12 +258,17 @@ function checkFallbacks(file: string, at: string, fallbacks: Fallback[], fallbac
246
258
  return out;
247
259
  }
248
260
 
249
- /** The stages a review of `stage` answers for: its own, and every stage after the previous reviewed one. The final review
250
- * covers red, green and refactor when none of them is reviewed. */
251
- function coveredStages(stage: Stage, policy: DeliveryPolicy): Stage[] {
261
+ /** The stages a review of `stage` answers for: its own, and every stage after the previous reviewed one (the final review
262
+ * covers red, green and refactor when none of them is reviewed). A stage with nothing written records nothing; when no
263
+ * work is recorded in that range, the review answers for the latest recorded work before it, so a plan-only final review
264
+ * is still independent of the plan's writer. */
265
+ function coveredStages(stage: Stage, policy: DeliveryPolicy, work: Work[]): Stage[] {
252
266
  const position = STAGES.indexOf(stage);
253
267
  const previous = STAGES.slice(0, position).findLastIndex(name => (policy.stages[name]?.reviewer.length ?? 0) > 0);
254
- return STAGES.slice(previous + 1, position + 1);
268
+ const range = STAGES.slice(previous + 1, position + 1);
269
+ if (work.some(entry => range.includes(entry.stage))) return range;
270
+ const latest = STAGES.slice(0, previous + 1).findLast(name => work.some(entry => entry.stage === name));
271
+ return latest ? [latest] : range;
255
272
  }
256
273
 
257
274
  /** Reports are data, never code: a report path is exempt from drift, so it must not be able to name a source file. */
@@ -291,7 +308,7 @@ function checkEvidence(file: string, evidence: Evidence, policy: DeliveryPolicy)
291
308
  out.push(...checkFallbacks(file, at, fallbacks, policy.fallback), ...checkModel(file, at, review.stage, "reviewer", review.reviewer, stage.reviewer, fallbacks, "reviewer", policy.fallback));
292
309
  if (review.author && stage.writer.length) out.push(...checkModel(file, at, review.stage, "writer", review.author, stage.writer, fallbacks, "reviewer", policy.fallback));
293
310
  // Independence is judged against everyone who wrote what this review answers for.
294
- const covered = coveredStages(review.stage, policy), writers = [...work.filter(entry => covered.includes(entry.stage)).map(entry => entry.writer), ...(review.author ? [review.author] : [])];
311
+ const covered = coveredStages(review.stage, policy, work), writers = [...work.filter(entry => covered.includes(entry.stage)).map(entry => entry.writer), ...(review.author ? [review.author] : [])];
295
312
  if (!writers.length) out.push(finding("delivery.evidence-schema", file, `${at}: names no writer of the work it reviews (${covered.join(", ")}); record each stage's work, or the review's author, so independence can be judged`));
296
313
  if (writerRuns.has(review.reviewer.run)) out.push(finding("delivery.reviewer-independent", file, `${at}: reviewer run '${review.reviewer.run}' also wrote in this tranche; a reviewer that edits becomes a writer`));
297
314
  if (reviewerRuns.has(review.reviewer.run)) out.push(finding("delivery.reviewer-independent", file, `${at}: reviewer run '${review.reviewer.run}' already reviewed reviews[${reviewerRuns.get(review.reviewer.run)}]; every review is a fresh process`));
@@ -327,9 +344,6 @@ function checkEvidence(file: string, evidence: Evidence, policy: DeliveryPolicy)
327
344
  if (evidence.status === "ready") {
328
345
  const configured = STAGES.filter(stage => policy.stages[stage]);
329
346
  for (const stage of configured) {
330
- // A 0.9 record names writers as the authors of the reviews that covered their stages.
331
- const written = work.some(entry => entry.stage === stage) || reviews.some(review => review.author && coveredStages(review.stage, policy).includes(stage));
332
- if (policy.stages[stage]!.writer.length && !written) out.push(finding("delivery.stages-complete", file, `status is ready, but ${stage} records no work; name who wrote it`));
333
347
  if (!policy.stages[stage]!.reviewer.length) continue;
334
348
  const last = reviews.filter(review => review.stage === stage).at(-1);
335
349
  if (!last) out.push(finding("delivery.stages-complete", file, `status is ready, but ${stage} has no review`));
@@ -455,7 +469,8 @@ export async function validateDelivery(root = process.cwd(), options: DeliveryOp
455
469
  if (seen.has(review.report)) findings.push(finding("delivery.evidence-schema", entry.file, `reviews[${index}] reuses report ${review.report} from reviews[${seen.get(review.report)}]; every review retains its own report`));
456
470
  else seen.set(review.report, index);
457
471
  }
458
- findings.push(...checkEvidence(entry.file, entry.data, policy));
472
+ const judged = entry.data.status === "ready" && entry.data.approved_sha ? await policyAt(absolute, entry.data.approved_sha, policy) : policy;
473
+ findings.push(...checkEvidence(entry.file, entry.data, judged));
459
474
  if (entry.data.status === "ready") {
460
475
  // Ready means auditable: every review's raw output is retained and named.
461
476
  for (const [index, review] of entry.data.reviews.entries()) if (!review.report || !regularFile(join(absolute, review.report))) findings.push(finding("delivery.stages-complete", entry.file, `status is ready, but reviews[${index}] (${review.stage}) ${review.report ? `names report ${review.report}, which is not a regular file` : "retains no report"}`));
@@ -468,7 +483,7 @@ export async function validateDelivery(root = process.cwd(), options: DeliveryOp
468
483
  else if (!await inHistory(item.sha)) findings.push(finding("delivery.approved-sha-resolves", entry.file, `work[${index}] (${item.stage}) names ${item.sha.slice(0, 7)}, which is not in the history of approved_sha ${approved.slice(0, 7)}`));
469
484
  }
470
485
  let previous: { stage: Stage; sha: string } | null = null;
471
- for (const stage of STAGES.filter(name => policy.stages[name]?.reviewer.length)) {
486
+ for (const stage of STAGES.filter(name => judged.stages[name]?.reviewer.length)) {
472
487
  const last = entry.data.reviews.filter(review => review.stage === stage).at(-1);
473
488
  if (!last || last.verdict !== "approve") continue; // reported by delivery.stages-complete
474
489
  if ((await git(absolute, ["cat-file", "-e", `${last.sha}^{commit}`])).code) { findings.push(finding("delivery.approved-sha-resolves", entry.file, `${stage} approved ${last.sha}, which is not a commit in this repository's history`)); continue; }