yadflow 3.16.1 → 3.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [3.16.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.16.1...v3.16.2) (2026-08-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **checkpoint:** make a --retro-ship dry run honest and side-effect free ([b03704f](https://github.com/abdelrahmannasr/yadflow/commit/b03704fb0564b1510dca121a24853758431f9374)), closes [112/#142](https://github.com/abdelrahmannasr/yadflow/issues/142) [#167](https://github.com/abdelrahmannasr/yadflow/issues/167)
7
+ * **checkpoint:** name the shard path and the fold step after --retro-ship ([64c33b3](https://github.com/abdelrahmannasr/yadflow/commit/64c33b37171ee1cebf540b7a66c35ee56cdc61a7)), closes [#167](https://github.com/abdelrahmannasr/yadflow/issues/167) [#167](https://github.com/abdelrahmannasr/yadflow/issues/167)
8
+ * **skills:** read build-log as the folded + shard union ([4302de2](https://github.com/abdelrahmannasr/yadflow/commit/4302de2e5f569989e8fc51c0a165aaae5abe1625)), closes [#167](https://github.com/abdelrahmannasr/yadflow/issues/167)
9
+
1
10
  ## [3.16.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.16.0...v3.16.1) (2026-08-12)
2
11
 
3
12
 
@@ -191,7 +191,7 @@ export function retroShipRepos(root, storyFile) {
191
191
  return { names, source: names.length ? 'registry' : 'none' };
192
192
  }
193
193
 
194
- export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, today }) {
194
+ export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, today, dryRun = false }) {
195
195
  if (!epic || !story) { fail('--retro-ship needs <epic>/<story> (e.g. --retro-ship EP-foo/EP-foo-S01)'); return { ok: false }; }
196
196
  if (!repo) { fail('--retro-ship needs --repo <name> (the repo the story shipped in)'); return { ok: false }; }
197
197
  const epicDir = path.join(root, 'epics', epic);
@@ -203,9 +203,10 @@ export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, to
203
203
  // Evidence and the flip must land TOGETHER — the #112 no-drift invariant. Refuse unless the human has
204
204
  // already flipped the story frontmatter to a back-half status in the working tree; otherwise the ship
205
205
  // shard would commit while the artifact still says e.g. `approved` — the very drift #112 prevents.
206
- if (!BACK_HALF_STATUSES.has(readFrontmatter(storyFile).status)) {
206
+ const storyStatus = readFrontmatter(storyFile).status;
207
+ if (!BACK_HALF_STATUSES.has(storyStatus)) {
207
208
  fail(`${story} frontmatter is not at in-build|shipped`);
208
- hand(`set \`status: shipped\` in ${storyRel} first, then re-run — the ship and the flip land in one commit`);
209
+ hand(`set \`status: in-build\` or \`status: shipped\` in ${storyRel} first, then re-run — the ship and the flip land in one commit`);
209
210
  return { ok: false };
210
211
  }
211
212
 
@@ -238,7 +239,12 @@ export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, to
238
239
  if (res.reason === 'collision') {
239
240
  // Distinct names, ONE shard file (`buildShardName` sanitizes each component) — recording this one
240
241
  // would overwrite the other repo's ship record, so it is refused rather than silently clobbered.
241
- fail(`${repo} cannot be recorded: it shares a build-log shard name with ${res.repo ? `the already-recorded ${res.repo}` : `an existing shard (${path.basename(res.file)})`}`);
242
+ // `writeRetroShip` reports a clash EITHER as `repo` (a ship readShips can see) OR as `file` (a shard
243
+ // on disk it cannot parse) — and a malformed shard can carry a blank `repo`, satisfying neither. Name
244
+ // whichever it gave us; never index into the one it did not, or the refusal becomes a stack trace.
245
+ const clashedWith = res.repo ? `the already-recorded ${res.repo}`
246
+ : res.file ? `an existing shard (${path.basename(res.file)})` : 'an existing ship record';
247
+ fail(`${repo} cannot be recorded: it shares a build-log shard name with ${clashedWith}`);
242
248
  hand('recording it would overwrite that ship record — rename one of the repos in the registry, or record this ship through the normal ship/checkpoint flow');
243
249
  return { ok: false };
244
250
  }
@@ -249,11 +255,26 @@ export function recordRetroShip(root, { epic, story, repo, task, mergeCommit, to
249
255
  if (left.length) hand(`still unrecorded for ${story}: ${left.join(', ')} — re-run with \`--repo <name>\` for each`);
250
256
  return { ok: false };
251
257
  }
252
- ok(`recorded retroactive ship for ${story} (${repo})${mergeCommit ? ` @ ${mergeCommit}` : ''}`);
258
+ // A dry run WROTE this shard only so the flip could be previewed; it is rolled back before the command
259
+ // returns. Every line below must therefore speak in the conditional — a past-tense "recorded" would tell
260
+ // the operator the backfill landed, they would never re-run for real, and the story would keep
261
+ // `status: shipped` with no ship behind it: exactly the #112/#142 drift this command exists to remove.
262
+ ok(`${dryRun ? 'would record' : 'recorded'} retroactive ship for ${story} (${repo})${mergeCommit ? ` @ ${mergeCommit}` : ''}`);
263
+ // Name WHERE the record landed. The ledger is shard-then-fold, so this ship is one loose shard and the
264
+ // folded `build-log.json` is untouched until `yad tidy up` runs — an operator who opens build-log.json,
265
+ // finds nothing, and concludes the write was lost is reading half the ledger (#167). Every reader unions
266
+ // the two; say so here, while they are looking. `tidy up` only folds a story whose frontmatter is
267
+ // `shipped`, so an `in-build` story's shard stays loose (and still readable) by design — mention the
268
+ // `shipped` precondition only when it is actually still outstanding.
269
+ const rel = path.relative(root, res.file).split(path.sep).join('/');
270
+ info(`${dryRun ? 'would land at' : 'landed at'} ${rel}`);
271
+ info(`readers union build-log/ shards with the folded build-log.json; \`yad tidy up\` folds it in${storyStatus === 'shipped' ? '' : ` once ${story} is \`shipped\``}`);
253
272
  // A multi-repo story is only half-reconciled until every declared repo has evidence, and nothing
254
273
  // downstream reports the gap (the story already reads `shipped`) — so say it here, while the
255
- // operator is running the backfill.
256
- if (left.length) hand(`${story} declares ${names.length} repos still unrecorded: ${left.join(', ')}; re-run with \`--repo <name>\` for each`);
274
+ // operator is running the backfill. `left` excludes the repo just recorded; in a dry run that record is
275
+ // rolled back, so it is still unrecorded and belongs back in the list.
276
+ const stillLeft = dryRun && left.length ? [repo, ...left] : left;
277
+ if (stillLeft.length) hand(`${story} declares ${names.length} repos — still unrecorded: ${stillLeft.join(', ')}; re-run with \`--repo <name>\` for each`);
257
278
  return { ok: true, file: res.file };
258
279
  }
259
280
 
@@ -288,22 +309,30 @@ export async function runCheckpoint(root, opts = {}) {
288
309
  // already wrote is then carried by the normal storyStatusPathspecs path below — no raw git needed.
289
310
  let retroFile;
290
311
  if (opts.retroShip) {
291
- const r = recordRetroShip(root, opts.retroShip);
312
+ const r = recordRetroShip(root, { ...opts.retroShip, dryRun: opts.dryRun });
292
313
  if (!r.ok) { process.exitCode = 1; return; }
293
314
  retroFile = r.file;
294
315
  }
295
316
 
317
+ // A --retro-ship DRY RUN wrote a real shard so the flip could be previewed, and it must be undone on
318
+ // EVERY exit path below, not just the happy one. An early return that skipped the rollback (git add
319
+ // failed, nothing staged) would leave an untracked retro shard behind — which then refuses the
320
+ // operator's next REAL backfill ("already has a build-log ship in <repo>") and rides into the next
321
+ // plain `yad checkpoint` as permanent `retroactive: true` audit evidence nobody chose to record.
322
+ // A real run deliberately keeps its shard on a failure (see the commit-failed path below).
323
+ const rollbackRetro = () => { if (opts.dryRun && retroFile) cleanupRetroShard(retroFile); };
324
+
296
325
  // The machine ledgers PLUS any build-log-backed story `status:` flip (#112) — one commit records
297
326
  // both, so the story artifact never drifts from build-log and no raw git-to-main push is needed.
298
327
  const pathspecs = [...backHalfPathspecs(root), ...storyStatusPathspecs(root)];
299
- if (!pathspecs.length) { info('no back-half ledgers found — nothing to checkpoint'); return; }
328
+ if (!pathspecs.length) { rollbackRetro(); info('no back-half ledgers found — nothing to checkpoint'); return; }
300
329
 
301
330
  // Stage the allowlist. `git add -- <spec>` picks up new + modified files, and deletions of tracked
302
331
  // files WITHIN a still-present spec (e.g. a removed build-state/<story>.json). A wholesale-deleted
303
332
  // top-level ledger is intentionally NOT staged (its spec drops out on the existence check) — an
304
333
  // append-only audit ledger vanishing is an anomaly a human should see, not something to auto-commit.
305
334
  const add = git('add', '--', ...pathspecs);
306
- if (!add.ok) { fail(`git add failed — ${add.stderr.split('\n')[0] || add.code}`); process.exitCode = 1; return; }
335
+ if (!add.ok) { rollbackRetro(); fail(`git add failed — ${add.stderr.split('\n')[0] || add.code}`); process.exitCode = 1; return; }
307
336
 
308
337
  // #112 review-bypass guard: a story is only carried when its staged change is the `status:` line
309
338
  // ALONE. Unstage any candidate whose working tree also touched prose/other frontmatter — those
@@ -318,6 +347,7 @@ export async function runCheckpoint(root, opts = {}) {
318
347
  }
319
348
 
320
349
  if (git('diff', '--cached', '--quiet', '--', ...pathspecs).ok) {
350
+ rollbackRetro();
321
351
  info('back-half state unchanged — nothing to commit');
322
352
  return;
323
353
  }
@@ -336,7 +366,7 @@ export async function runCheckpoint(root, opts = {}) {
336
366
  git('reset', '-q', '--', ...pathspecs); // restore the index — a dry run must not leave things staged
337
367
  // A --retro-ship dry run wrote a shard so the flip could be PREVIEWED above; undo it now so the dry
338
368
  // run leaves no side effect on disk (git reset only unstaged it, back to untracked).
339
- if (retroFile) cleanupRetroShard(retroFile);
369
+ rollbackRetro();
340
370
  info('dry run — not committed');
341
371
  return { message };
342
372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "3.16.1",
3
+ "version": "3.16.2",
4
4
  "description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -139,7 +139,12 @@ build:
139
139
  risk_levels: [low, medium, high] # high (or a contract/auth/payments surface) routes to domain owners (yad-review-gate escalation)
140
140
  # Step E (yad-engineer-review) — AI review (advisory) + engineer review (the human gate) + merge.
141
141
  ai_review: coderabbit # advisory first pass; never the authority (.coderabbit.yaml)
142
- build_log: "epics/EP-<slug>/.sdlc/build-log.json" # append-only ship ledger (back-half analogue of approvals.json)
142
+ # Append-only ship ledger (back-half analogue of approvals.json), stored shard-then-fold: writers add one
143
+ # shard per ship under build_log_dir; `yad tidy up` folds a SHIPPED story's shards into build_log.
144
+ # READERS MUST UNION the two (dedupe by (story, task, repo); a shard wins) — build_log alone omits every
145
+ # unfolded ship, e.g. a `yad checkpoint --retro-ship` backfill or any ship on an `in-build` story.
146
+ build_log: "epics/EP-<slug>/.sdlc/build-log.json" # folded ships
147
+ build_log_dir: "epics/EP-<slug>/.sdlc/build-log/" # one <story>-<task>-<repo>.json shard per ship
143
148
  story_build_states: [in-build, shipped] # in-build = some tasks shipped; shipped = all tasks in tasks.md shipped
144
149
  # Backfill (yad-backfill) — specs for already-built features in an existing repo.
145
150
  backfill:
@@ -33,7 +33,7 @@ SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive do
33
33
  SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
34
34
  SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
35
35
  SDLC Workflow,yad-change,Change/Defect Intake,CH,"Phase 6 post-lock change management: the INTAKE + TRIAGE step of a feature thread. Classifies the change DEPTH (defect-fix / behavioral-no-surface / contract-surface / new-capability), seeds a NEW EP-<slug> change-epic threaded to its parent (lineage frontmatter kind/parent/thread/inherits/supersedes + a state.json whose inherited steps are pre-marked done and only the changed steps run; a pointer-lock contract-lock.json when architecture is inherited), and records the intake in change.json (escape_stage + root_cause for defects). For hotfixes it records the ship-first exception and opens reconcile-debt.json. Never auto-advances — hands off to the normal authoring skills + yad-review-gate.",,{parent: EP-<slug>} {title: one-line} {kind: change|defect|hotfix} {origin: production|staging|qa|review} {severity: sev1..sev4} {description: text} {affected: artifacts},1-front,,yad-review-gate,false,epics/EP-<slug>/,epic.md state.json change.json reconcile-debt.json contract-lock.json
36
- SDLC Workflow,yad-timeline,Feature Timeline,TL,"Render a feature THREAD (its linked epics, genesis->changes->defects) as an evolution view (the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary) AND resolve the inheritance chain into the authoritative current artifact set (thread-resolved.md: the winning source per artifact + the resolved contract-lock hash) — the composed source-of-truth AI/humans read for the next change. Reads frontmatter lineage + each change.json + build-log.json. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<genesis>/,timeline-site/ thread-resolved.md TIMELINE.md
37
- SDLC Workflow,yad-defects,Quality-Gap Report,DF,"Generate a per-epic AND per-thread defect/bug report (same vendored shell + DEFECTS.md). Walks the thread for every kind:defect change-epic + each change.json defect block + shipped regressions in build-log.json, aggregates by escape_stage (the SDLC gate that should have caught it) and root_cause, and visualizes WHERE quality gaps systematically come from (e.g. % of thread defects that escaped at the test-cases gate) so the team can harden the originating stage. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{epic: EP-<slug> | thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<slug>/,defects-site/ DEFECTS.md
36
+ SDLC Workflow,yad-timeline,Feature Timeline,TL,"Render a feature THREAD (its linked epics, genesis->changes->defects) as an evolution view (the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary) AND resolve the inheritance chain into the authoritative current artifact set (thread-resolved.md: the winning source per artifact + the resolved contract-lock hash) — the composed source-of-truth AI/humans read for the next change. Reads frontmatter lineage + each change.json + the build ledger (folded build-log.json UNIONed with every build-log/ shard). An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<genesis>/,timeline-site/ thread-resolved.md TIMELINE.md
37
+ SDLC Workflow,yad-defects,Quality-Gap Report,DF,"Generate a per-epic AND per-thread defect/bug report (same vendored shell + DEFECTS.md). Walks the thread for every kind:defect change-epic + each change.json defect block + shipped regressions in the build ledger (folded build-log.json UNIONed with every build-log/ shard), aggregates by escape_stage (the SDLC gate that should have caught it) and root_cause, and visualizes WHERE quality gaps systematically come from (e.g. % of thread defects that escaped at the test-cases gate) so the team can harden the originating stage. An OUTPUT ENRICHMENT — never a gate; never mutates state.",,{epic: EP-<slug> | thread: EP-<genesis>} {action: generate|deploy},,,,false,epics/EP-<slug>/,defects-site/ DEFECTS.md
38
38
  SDLC Workflow,yad-reconcile,Change Reconciler,RE,"Maintenance/CI (mirrors yad-docs-sync — never a gate): detect post-lock DRIFT/ORPHANS — shipped code or a repo HEAD advance (the repos.json syncedHead-vs-current-HEAD rule) with NO owning change-epic in any thread — plus open hotfix reconcile debt, and report which thread drifted and why. refresh points at yad-change to open a reconcile change-epic (or yad-stub first, to anchor orphan brownfield code that has no epic at all) — never silent; wire commits advisory CI ([skip ci] + concurrency, like yad-docs-sync). The actual merge BLOCK is the lineage-check / reconcile-debt gates; this only discovers.",,{action: check|refresh|wire} {thread: EP-<genesis>},,,,false,epics/EP-<genesis>/.sdlc/,(report) reconcile-debt.json yad-reconcile.yml
39
39
  SDLC Workflow,yad-stub,Stub Genesis Epic,SG,"Phase 6 brownfield helper: mint a STUB genesis epic for an already-built feature that has no epic in the hub, so a defect/change can thread off it TODAY (yad-change requires a real parent and dead-ends without one). Creates the smallest real thread anchor — a tiny epic.md (kind:feature, thread:self, verified:false, stub:backfill-pending) + a seeded state.json (kind:stub / currentStep:backfill-pending) + empty ledgers — never inventing behaviour. Defects thread off it immediately (gates pass; the bug list is derived by the thread rollup); yad-backfill + its promote step later flip it into a real feature epic. Never auto-advances.",,{feature: <name>} {repos: [<repo>]} {description: one-line},1-front,,yad-change,false,epics/EP-<slug>/,epic.md state.json approvals.json comments.json
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yad-defects
3
- description: 'Phase 6 output enrichment (never a gate) — the quality-gap report. Generates a per-epic AND per-thread defect/bug report (the vendored React/Vite/Tailwind shell HTML + a DEFECTS.md) that aggregates every kind:defect change-epic + each change.json defect block + shipped regressions in build-log.json BY escape_stage (the SDLC gate that should have caught the defect) and root_cause, and visualizes WHERE quality gaps systematically come from — e.g. "% of this feature''s defects that escaped at the test-cases gate" — so the team hardens the originating stage instead of just fixing symptoms. Degrades to markdown-only when no docs target is connected. Use when the user says "show the defect report", "where are our quality gaps", "generate the bug report for this epic", or "which gate is leaking defects".'
3
+ description: 'Phase 6 output enrichment (never a gate) — the quality-gap report. Generates a per-epic AND per-thread defect/bug report (the vendored React/Vite/Tailwind shell HTML + a DEFECTS.md) that aggregates every kind:defect change-epic + each change.json defect block + shipped regressions in the build ledger (the folded build-log.json unioned with every build-log/ shard) BY escape_stage (the SDLC gate that should have caught the defect) and root_cause, and visualizes WHERE quality gaps systematically come from — e.g. "% of this feature''s defects that escaped at the test-cases gate" — so the team hardens the originating stage instead of just fixing symptoms. Degrades to markdown-only when no docs target is connected. Use when the user says "show the defect report", "where are our quality gaps", "generate the bug report for this epic", or "which gate is leaking defects".'
4
4
  ---
5
5
 
6
6
  # SDLC — Quality-Gap Report (Phase 6, output enrichment)
@@ -32,10 +32,16 @@ risk), not just the symptom. It is an **output enrichment**, exactly like `yad-d
32
32
  Resolve the scope (`yad thread <id> --json` for a thread). Collect, across the scoped epic(s):
33
33
  - every `kind: defect` (and `kind: hotfix`) change-epic + its `.sdlc/change.json` `defect` block
34
34
  (`origin`, `severity`, `escape_stage`, `root_cause`);
35
- - the shipped regression fixes from each `.sdlc/build-log.json` (the fix that closed the defect, linking
35
+ - the shipped regression fixes from each epic's build ledger (the fix that closed the defect, linking
36
36
  the change-epic → its regression story/test);
37
37
  - open reconcile debt (a hotfix whose front truth is not yet restored).
38
38
 
39
+ The build ledger is **shard-then-fold**: read it as the **union** of the folded `.sdlc/build-log.json`
40
+ `ships` PLUS every loose `.sdlc/build-log/` shard, deduped by `(story, task, repo)` — a shard WINS over a
41
+ folded ship of the same key. Reading `build-log.json` alone drops every ship not yet folded by
42
+ `yad tidy up` (including every `yad checkpoint --retro-ship` backfill, and every ship on a story still at
43
+ `in-build`, which `tidy up` never folds), which would under-count the very fixes this report attributes.
44
+
39
45
  ### Step 2 — Attribute each defect to the gate that should have caught it
40
46
  A defect is attributed to its **earliest** responsible SDLC stage. Use `change.json.escape_stage`
41
47
  (human-set at intake), cross-checked against the fix's shape: a missing negative test → `test-cases`; a
@@ -73,7 +79,9 @@ Also write a plain `epics/<scope>/DEFECTS.md` mirror. On `action: deploy`, `yad
73
79
  - **Degrade gracefully.** No docs target → `DEFECTS.md` only; never fail because a tool is absent.
74
80
 
75
81
  ## Reference
76
- - The defect data: `.sdlc/change.json` `defect` blocks + `build-log.json` (`../yad-epic/references/state-schema.md`, Phase 6).
82
+ - The defect data: `.sdlc/change.json` `defect` blocks + the build ledger, read as the folded
83
+ `build-log.json` unioned with every `.sdlc/build-log/` shard
84
+ (`../yad-epic/references/state-schema.md`, Phase 6; `../yad-engineer-review/references/ship-and-record.md`).
77
85
  - The shell + deterministic generation it reuses: `../yad-docs/SKILL.md`, `../yad-docs/references/data-mapping.md`.
78
86
  - The companion evolution view: `../yad-timeline/SKILL.md`.
79
87
  - The intake that records `escape_stage` + `root_cause`: `../yad-change/SKILL.md`.
@@ -69,7 +69,7 @@ Per-story, per-repo: `spec → tasks → implement → checks → engineer-revie
69
69
  | `yad-checks` | `checks/*.sh`, CI workflows — the gate set: `spec-link · contract-check · build-test-lint · verified-commits · commit-message · pr-title · pr-template · lineage-check · epic-open · reconcile-debt`, plus `yad-update-guard` (push-on-default: re-checks any direct-to-default commit with `verified-commits · commit-message`) |
70
70
  | `yad-pr-template` | PR/MR template + routing helpers |
71
71
  | `yad-commit` / `yad-open-pr` / `yad-ship` | one commit / one PR/MR |
72
- | `yad-engineer-review` | engineer review + ship recorded in `build-log.json` |
72
+ | `yad-engineer-review` | engineer review + ship recorded as a `build-log/` shard |
73
73
  | `yad-backfill` | DRAFT specs for legacy features |
74
74
 
75
75
  ### Path: Automation (the second dial + observation)
@@ -110,6 +110,11 @@ and the change-thread ledgers `change.json`, `reconcile-debt.json`, `build-log.j
110
110
  the **connected code repos**; the **design / testing / learning tools**; and the **platform**
111
111
  (GitHub/GitLab + Pages). A skill's `sideEffects` link its step to the component it writes.
112
112
 
113
+ `trust-log.json` and `build-log.json` are the *folded* halves of two **shard-then-fold** ledgers — each
114
+ also has a shard dir (`.sdlc/trust-log/`, `.sdlc/build-log/`) holding the entries `yad tidy up` has not
115
+ folded yet. They render as one component each, but anything READING them must union the folded file with
116
+ its shards (`../../yad-engineer-review/references/ship-and-record.md`).
117
+
113
118
  ## Roles = the lenses
114
119
 
115
120
  The eight yadflow lenses, each to its relevant phase sections + paths:
@@ -83,8 +83,15 @@ It writes ONE minimal ship shard marked `retroactive: true` (`task` defaults to
83
83
  the normal checkpoint so the story's already-made `status:` flip rides along in the **same** commit. It
84
84
  refuses when the story already has a ship **in that repo** (then it isn't pre-tracking there — use the
85
85
  normal flow). It does **not** author the story frontmatter — and to keep evidence and the flip atomic (the no-drift
86
- invariant), it **refuses** unless you have already set `status: shipped` in `stories/<story>.md`, so a
87
- ship shard is never committed while the artifact still says `approved`.
86
+ invariant), it **refuses** unless you have already set a back-half `status:` (`in-build` or `shipped`) in
87
+ `stories/<story>.md`, so a ship shard is never committed while the artifact still says `approved`.
88
+
89
+ **Where the record lands — it is a shard, not an append to `build-log.json`.** Like every other ship, a
90
+ retroactive one is written to `.sdlc/build-log/` and the folded `build-log.json` is left untouched until
91
+ `yad tidy up` folds it. Opening `build-log.json` and finding nothing does **not** mean the write was lost:
92
+ read the ledger by the union rule above and the ship is there (#167). Note `tidy up` only folds a story
93
+ whose frontmatter is `shipped`, so a backfill recorded against an `in-build` story stays a loose shard
94
+ indefinitely — which is precisely why reading the folded file alone is never sufficient.
88
95
 
89
96
  **One repo per run (#166).** A ship is recorded per `(story, task, repo)`, so a story that shipped in
90
97
  several repos needs one retroactive shard **per repo** — the guard is keyed on `(story, repo)`, not on
@@ -138,11 +145,12 @@ The story frontmatter `status` reflects build progress:
138
145
  You write this flip into `stories/<story>.md`, but **do not hand-commit it** — the next
139
146
  `yad checkpoint --push` carries it in the same `chore(hub)` commit as the ledgers (the story now has a
140
147
  build-log ship, so checkpoint stages it; #112). This is what keeps the story artifact from drifting
141
- from `build-log.json`, so there is never a reason to fall back to a raw `git push origin main`.
148
+ from the build ledger, so there is never a reason to fall back to a raw `git push origin main`.
142
149
 
143
150
  So the chain is traceable both ways: from the epic down (`epic.md` → `stories/<story>.md` →
144
- `tasks.md` → `build-log.json` ship → `mergeCommit`) and from a merge commit back up (its `Task:`
145
- trailer → story → epic).
151
+ `tasks.md` → the build-ledger ship → `mergeCommit`) and from a merge commit back up (its `Task:`
152
+ trailer → story → epic). Resolve that ship by the union rule, not from `build-log.json` alone — until
153
+ `yad tidy up` folds it, the ship lives only in its `build-log/` shard and the chain looks broken.
146
154
 
147
155
  ## Preconditions for ship (all required)
148
156
 
@@ -325,11 +325,17 @@ storage layout is noted here (it mirrors `trust-log.json`):
325
325
  ship object. `(story, task, repo)` is already a natural unique key, so no `uid` is needed.
326
326
  - **Folded file:** `epics/<epic>/.sdlc/build-log.json` = `{ "epic": "<id>", "ships": [ <ship>, … ] }`
327
327
  (also the legacy single-file layout, and the output of `yad tidy up`).
328
- - **Union-read rule:** union the folded `ships` with every `build-log/` shard, **deduping by
329
- `(story, task, repo)`** — a shard WINS over a stale folded ship (so a `yad review reconcile` edit to a
330
- ship's shard is authoritative until it is folded).
328
+ - **Union-read rule (binding on every reader — never read the folded file alone):** union the folded
329
+ `ships` with every `build-log/` shard, **deduping by `(story, task, repo)`** — a shard WINS over a stale
330
+ folded ship (so a `yad review reconcile` edit to a ship's shard is authoritative until it is folded).
331
+ `build-log.json` on its own is only the *folded* half of the ledger: it omits every ship `yad tidy up`
332
+ has not folded, including every `yad checkpoint --retro-ship` backfill, and — because `tidy up` folds
333
+ only a story whose frontmatter is `shipped` — every ship on a story still at `in-build`, indefinitely.
334
+ A reader that skips the union silently under-reports what shipped (#167).
331
335
  - `yad checkpoint` commits the shard dir; `yad tidy up` folds a shipped story's finished shards into the
332
- folded file (loose objects + `git gc`).
336
+ folded file (loose objects + `git gc`). No **ship** path ever appends to the folded file — that is what
337
+ keeps concurrent shippers conflict-free. (`yad review reconcile` does write it, but only to stamp a ship
338
+ already folded there; see `updateShip`.)
333
339
 
334
340
  ---
335
341
 
@@ -34,7 +34,7 @@ Run `yad reconcile check` (optionally `--thread EP-<genesis>`). For each thread
34
34
  `yad check` drift style, any of:
35
35
  - **Broken lineage** — a change-epic whose `parent` is missing, a cycle, or a `thread` cache that
36
36
  disagrees with the computed root (the same signal `yad doctor` reports).
37
- - **Orphan / drift** — code shipped (`build-log.json`) or a touched repo's HEAD advanced past its
37
+ - **Orphan / drift** — code shipped (the build ledger) or a touched repo's HEAD advanced past its
38
38
  `repos.json` `syncedHead` with **no owning change-epic** in any thread — i.e. behaviour reached
39
39
  production that no epic in the thread describes. Name the repo (`<repo>: <old>→<new>`).
40
40
  - **Open hotfix debt** — a `reconcile-debt.json` entry still `open`; the next normal change on that
@@ -42,6 +42,14 @@ Run `yad reconcile check` (optionally `--thread EP-<genesis>`). For each thread
42
42
 
43
43
  Writes nothing. This is the read-only sweep a human (or CI) runs to see the picture.
44
44
 
45
+ > **Read the build ledger as a union — a missed ship reads as "no drift".** It is **shard-then-fold**:
46
+ > union the folded `.sdlc/build-log.json` `ships` with every loose `.sdlc/build-log/` shard, deduped by
47
+ > `(story, task, repo)` (a shard WINS over a folded ship of the same key). Reading `build-log.json` alone
48
+ > drops every ship not yet folded by `yad tidy up` — including every `yad checkpoint --retro-ship` backfill,
49
+ > and every ship on a story still at `in-build`, which `tidy up` never folds. Those are exactly the ships
50
+ > most likely to be orphaned, so missing them turns this check into a false all-clear. See
51
+ > `../yad-engineer-review/references/ship-and-record.md`.
52
+
45
53
  ### Step 2 — `refresh` (advisory, never silent)
46
54
  For each flagged thread, **point the human at the fix** — open a reconcile change-epic with `yad-change`
47
55
  (`kind: change`, threaded to the affected feature) to bring the front artifacts back in step with what
@@ -38,9 +38,15 @@ current-truth map, and any open reconcile debt. **STOP** and report if the linea
38
38
 
39
39
  ### Step 2 — Read each node's evolution facts
40
40
  For each epic in the chain read `epic.md` (lineage + the change brief), `.sdlc/change.json` (depth,
41
- defect block), `.sdlc/build-log.json` (ship events), and the contract-lock (a real lock = a re-lock
41
+ defect block), the build ledger (ship events), and the contract-lock (a real lock = a re-lock
42
42
  event; a pointer-lock = inherited). Greenfield-safe: an absent input degrades its part of the view.
43
43
 
44
+ The build ledger is **shard-then-fold**, so read it as the **union** of the folded `.sdlc/build-log.json`
45
+ `ships` PLUS every loose `.sdlc/build-log/` shard, deduped by `(story, task, repo)` — a shard WINS over a
46
+ folded ship of the same key. Reading `build-log.json` alone silently drops every ship not yet folded by
47
+ `yad tidy up` (including every `yad checkpoint --retro-ship` backfill, and every ship on a story still at
48
+ `in-build`, which `tidy up` never folds). See `../yad-engineer-review/references/ship-and-record.md`.
49
+
44
50
  ### Step 3 — Render the evolution view (yad-docs shell)
45
51
  Generate the site into `epics/<thread>/timeline-site/` (copy the shell verbatim; generate `src/data/*.ts`
46
52
  deterministically; theme from the design system). The thread maps onto the shell primitives: