yadflow 3.13.1 → 3.13.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,10 @@
1
+ ## [3.13.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.13.1...v3.13.2) (2026-08-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **doctor:** fail a done review gate that holds no approval ([b1b23df](https://github.com/abdelrahmannasr/yadflow/commit/b1b23df5426e8523cdb1d95221cb2291200c6249))
7
+
1
8
  ## [3.13.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.13.0...v3.13.1) (2026-07-29)
2
9
 
3
10
 
package/cli/doctor.mjs CHANGED
@@ -368,20 +368,40 @@ function contractLockCheck(checks, root, epic, ledger) {
368
368
  check(checks, id, 'epics', 'ok', `${epic}: contract surface matches its lock (${short(stored)})`);
369
369
  }
370
370
 
371
- // A review step that is `done` but whose approvals no longer bind to the artifact as it stands today.
372
- // The gate is deliberately one-way — nothing pulls a chain backward once work is built on it — so the
373
- // only way this surfaces is if something reports it. `gate sync` records the gap on the step it is
374
- // syncing; this reports it for the whole epic, so a re-locked surface that was never re-approved is
375
- // visible in the one command people run when something looks wrong. WARN, not FAIL: the state is a
376
- // fact about history, and the fix (re-open the review) is a human decision.
377
- function staleGateCheck(checks, root, epic, ledger) {
371
+ // Two findings on a review step that is already `done`, both about the approval record behind it.
372
+ //
373
+ // FAIL it holds NO qualifying approval at all (outside solo mode). This is the state the gate
374
+ // exists to prevent: the step advanced without the record that justifies it. `gatePredicate` counts
375
+ // exactly the same thing (`status === 'approved'`, with `inherited`/`skipped` steps short-circuited
376
+ // before it), so a step doctor reports here is one the gate itself would refuse today.
377
+ //
378
+ // WARN — it holds approvals, but none still bind to the artifact as it stands. The gate is
379
+ // deliberately one-way — nothing pulls a chain backward once work is built on it — so the only way
380
+ // this surfaces is if something reports it. `gate sync` records the gap on the step it is syncing;
381
+ // this reports it for the whole epic, so a re-locked surface that was never re-approved is visible
382
+ // in the one command people run when something looks wrong. A warning, because the state is a fact
383
+ // about history and the fix (re-open the review) is a human decision.
384
+ function staleGateCheck(checks, root, epic, ledger, { solo = false } = {}) {
378
385
  const epicDir = epicRoot(root, epic);
379
386
  for (const s of ledger.state.steps) {
380
387
  if (s.type !== 'review+approve' || s.status !== 'done' || s.inherited || s.skipped) continue;
388
+ const forStep = ledger.approvals.filter((a) => a.step === s.id && a.status === 'approved');
389
+ // Checked BEFORE the artifact hash below: "done holding no approval" is a claim about the ledger,
390
+ // not about content, so it must not depend on there being something to hash. Gating it behind the
391
+ // hash would keep hiding it on every epic with no locked surface.
392
+ if (!forStep.length) {
393
+ // Solo mode waives the approval requirement outright (you cannot approve your own PR) — the
394
+ // merge + resolved threads are what advance the step, so an empty record is the documented
395
+ // shape there, not a finding. Everywhere else it is the gate being silently defeated.
396
+ if (solo) continue;
397
+ const records = ledger.approvals.filter((a) => a.step === s.id).length;
398
+ check(checks, `epic:${epic}:${s.id}:unapproved`, 'epics', 'fail',
399
+ `${epic}: ${s.id} is done but holds no approval${records ? ` (${records} record(s), none of them live)` : ''}`,
400
+ 'the step advanced without the record the gate exists to keep — re-open the review (a fresh PR/MR) and re-approve, or run `yad gate sync` if the approvals are on the PR but never reached the ledger');
401
+ continue;
402
+ }
381
403
  const cur = artifactHash(epicDir, s.artifact);
382
404
  if (!cur) continue; // nothing to bind to (no locked surface / incomplete set) — not a staleness claim
383
- const forStep = ledger.approvals.filter((a) => a.step === s.id && a.status === 'approved');
384
- if (!forStep.length) continue; // solo mode waives approvals entirely; absence is not staleness
385
405
  const live = forStep.filter((a) => !a.artifactHash || a.artifactHash === cur);
386
406
  if (live.length) continue;
387
407
  check(checks, `epic:${epic}:${s.id}:stale`, 'epics', 'warn',
@@ -393,6 +413,8 @@ function staleGateCheck(checks, root, epic, ledger) {
393
413
  export function epicChecks(checks, root) {
394
414
  const epicsDir = path.join(root, 'epics');
395
415
  if (!exists(epicsDir)) return;
416
+ // Read once for the whole sweep: whether approval is waived is a project fact, not a per-epic one.
417
+ const solo = isSolo(readJSON(path.join(root, PROJECT_FILES.hubConfig), null));
396
418
  for (const e of fs.readdirSync(epicsDir).sort()) {
397
419
  if (!fs.statSync(path.join(epicsDir, e)).isDirectory()) continue;
398
420
  try {
@@ -419,7 +441,7 @@ export function epicChecks(checks, root) {
419
441
  `${e}: an open review PR (${openPr.artifact}${openPr.number ? ` #${openPr.number}` : ''}) is recorded on the default branch`,
420
442
  'opened under a pre-3.0 yadflow? merge/close it before continuing — CI now records the gate ledger on the default branch only at merge');
421
443
  contractLockCheck(checks, root, e, ledger);
422
- staleGateCheck(checks, root, e, ledger);
444
+ staleGateCheck(checks, root, e, ledger, { solo });
423
445
  }
424
446
  } catch (err) {
425
447
  check(checks, `epic:${e}`, 'epics', 'fail', `${e}: ${err.message} [${err.code || 'YAD-STATE-001'}]`, err.hint || 'fix the file or restore it from git');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "3.13.1",
3
+ "version": "3.13.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",