yadflow 3.13.0 → 3.13.1

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.
@@ -19,7 +19,8 @@
19
19
  # - MERGE push (default branch, commit names a review branch): resolve the merged MR's IID from its
20
20
  # source branch, then advance + flip status on the default branch — near-immediate.
21
21
  # - SCHEDULED sweep (catch-up): enumerate recently-merged review MRs via the API and advance any
22
- # not yet advanced (idempotent — a step already `done` is skipped). Covers bare approvals and
22
+ # not yet advanced (idempotent — an already-`done` step is never re-advanced, and re-syncing it
23
+ # writes nothing unless its approvals changed). Covers bare approvals and
23
24
  # squash merges whose commit message dropped the branch name. Create a pipeline schedule
24
25
  # (one-time, cannot be committed as code): cron `*/15 * * * *` with variable SDLC_GATE_SYNC=true.
25
26
  # UI: CI/CD > Schedules, or:
@@ -39,6 +40,15 @@ variables:
39
40
 
40
41
  yad-gate-sync:
41
42
  needs: []
43
+ # Serialize every advance: the merge-push pipeline and the scheduled sweep both commit the ledger
44
+ # and push the DEFAULT branch, and the sweep's 7-day window covers MRs the merge path is still
45
+ # processing — so without this they race on the same MR. Both produce the advance, one pushes, the
46
+ # loser rebases onto it and pushes the same commit message under a different SHA. That is where the
47
+ # duplicate `chore(gate): advance …` commits with shifting SHAs come from, and every one of them
48
+ # carries [skip ci] and lands directly on the default branch, so nothing validates the churn.
49
+ # `resource_group` is GitLab's equivalent of the `concurrency` group the GitHub workflow sets on both
50
+ # of its jobs: at most one instance runs at a time, the rest queue.
51
+ resource_group: yad-gate-mergesync
42
52
  # Runner selection: set the YAD_RUNNER_TAGS CI/CD variable (e.g. `dind_runner`) to route this
43
53
  # docker job on instances whose runners are tag-locked (run_untagged: false) — it lives in
44
54
  # project Settings → CI/CD → Variables, so it survives every `yad` sync. When unset, current
@@ -69,7 +79,8 @@ yad-gate-sync:
69
79
  rc=0
70
80
  if [ "$CI_PIPELINE_SOURCE" = "schedule" ]; then
71
81
  # SCHEDULED SWEEP — discover merged review MRs from the platform (Path B keeps no per-branch
72
- # ledger), then advance each. `gate ci` is idempotent: a step already `done` is skipped.
82
+ # ledger), then advance each. `gate ci` is idempotent: an already-`done` step is never
83
+ # re-advanced, and re-syncing it writes nothing unless its approvals changed.
73
84
  # A stuck review MR (a squash merge whose commit dropped the branch name, or a failed merge
74
85
  # push) is always RECENT, so sweep a generous recent window and PAGINATE it fully (--paginate)
75
86
  # — this bounds cost without the old hard 50-row cap that could permanently strand older MRs.
@@ -131,8 +131,9 @@ via the local user's `gh`/`glab`. For each:
131
131
  - an `APPROVED` review / MR approval → append an `approved` record to `approvals.json` tagged
132
132
  `"source": "bridge"`; a `COMMENTED`/`CHANGES_REQUESTED`/note → write to
133
133
  `reviews/<artifact-base>--<YYYY-MM-DD>--comments.md` + `comments.json` (never an approval).
134
- **Idempotent:** upsert bridge approvals by `(step, approver, role, domain)`, supersede revoked ones, and
135
- key comments on the platform comment id (re-running `sync` does not duplicate). **Manual approvals (no
134
+ **Idempotent:** upsert bridge approvals by `(step, approver, role, domain)`, supersede revoked ones
135
+ **while the step is open** (a step already `done` keeps its approvals they are the record of why it
136
+ passed), and key comments on the platform comment id (re-running `sync` does not duplicate). **Manual approvals (no
136
137
  `source` tag) are never touched.** For the architecture+contract step, discard bridge approvals dated
137
138
  before a new contract lock (re-lock invalidates platform approvals too). Then refresh the `approved.md`
138
139
  roster, set `hub-prs.json` `lastSyncedAt`, and **re-evaluate Step 3**. Under the PR-driven CLI (`yad
@@ -170,7 +171,9 @@ If the predicate **passes**:
170
171
 
171
172
  ### PR-driven automation (the `yad gate` CLI)
172
173
  When the hub has a platform, **CI is the sole writer of the ledger**. `yad gate open` opens the review
173
- PR only; CI (`yad gate ci`) writes the `.sdlc/` + `reviews/` records this skill describes. The skill's
174
+ PR only against the `review/<epic>/<artifact>` branch, which must already exist (create it and run
175
+ `yad open-pr` from it, which pushes it first). CI (`yad gate ci`) writes the `.sdlc/` + `reviews/`
176
+ records this skill describes. The skill's
174
177
  job is the human half: presenting the artifact, helping the owner address comments, and narrating the
175
178
  gate. Local `yad gate sync` is advisory in bridge mode (reads the platform, prints status, writes
176
179
  nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it).
@@ -179,6 +182,13 @@ Under that CLI the gate **advances on merge**: a review PR/MR whose reviewer rul
179
182
  comment threads are **all resolved**, and which has been **merged** auto-marks the step `done` and
180
183
  unblocks the next step. (Until those three hold, the step stays `in_review`.)
181
184
 
185
+ **Re-reviewing a step that already advanced.** A step is advanced **once** — the chain is never pulled
186
+ backward. But a step that is `done` is still *synced*: when the artifact is edited (for architecture, a
187
+ re-locked contract surface) its prior approvals go stale, and the approvals arriving on the new review
188
+ PR/MR are recorded and bound to the new content. `yad gate status` then shows the truth — how many
189
+ approvals are live against what is in the file today, and how many were revoked — instead of a `done`
190
+ step whose approvals all belong to the version before the edit.
191
+
182
192
  The flow is **merge-driven** (wired by `yad-hub-bridge` `wire`): during review CI writes nothing — the
183
193
  platform PR/MR is the source of truth (native approvals + threads), and CI never touches the review
184
194
  branch (so an in-flight approval is never dismissed and required checks never strand). On the human
@@ -42,7 +42,9 @@ content. This prevents "approve, then quietly change it" (build plan §5 spirit)
42
42
  For the architecture+contract review there is a second, content-based staleness check: recompute the
43
43
  SHA-256 of the contract-surface block and compare it to `.sdlc/contract-lock.json`. A mismatch means
44
44
  the locked surface changed even if the file's mtime looks fine — approvals are stale, re-lock and
45
- re-approve. (Hash recipe: `yad-architecture/references/contract-format.md`.)
45
+ re-approve. (Hash recipe: `yad-architecture/references/contract-format.md`.) `yad` computes the
46
+ identical digest, so `yad doctor` performs this comparison for every epic and FAILs on a surface that
47
+ drifted from its lock — run it rather than recomputing by hand.
46
48
 
47
49
  ## Worked example — epic gate
48
50
 
@@ -91,8 +93,10 @@ approvals regardless of how they were recorded.
91
93
  - On PR/MR open the assignee is the committer and reviewers are the scope's `reviewer` + `domain-owner`
92
94
  members (minus the committer); the owner/author is recorded, not requested. See
93
95
  `../yad-hub-bridge/references/login-roster.md`.
94
- - `sync` is idempotent (upsert by `(step, approver, role, domain)`; supersede revoked; key comments on
95
- comment id) and never touches **manual** approvals.
96
+ - `sync` is idempotent (upsert by `(step, approver, role, domain)`; key comments on comment id) and
97
+ never touches **manual** approvals. A revoked approval is superseded **while the step is open**; once
98
+ the step is `done` its approvals are kept as the record of why it passed, and a re-sync only re-binds
99
+ new ones (see `../yad-hub-bridge/references/bridge.md` → "Idempotent re-sync").
96
100
  - The architecture+contract staleness rule applies to bridge approvals too: a re-lock discards bridge
97
101
  approvals dated before the new lock.
98
102
  - No platform / no CLI → the gate runs file-only with no error. Detail: `../yad-hub-bridge/references/bridge.md`.
@@ -68,7 +68,7 @@ story: EP-<slug>-S0N
68
68
  epic: EP-<slug>
69
69
  repo: <repo>
70
70
  feature-id: EP-<slug>-S0N
71
- product-repo: <absolute or relative path to the product repo>
71
+ product-repo: <path to the product repo — absolute, or relative to THIS file's dir (specs/<story>/)>
72
72
  contract-lock: sha256:<hex copied from epics/EP-<slug>/.sdlc/contract-lock.json>
73
73
  speckit: installed | not-installed
74
74
  generated: <YYYY-MM-DD>
@@ -86,6 +86,16 @@ The contract surface above is **referenced, not re-defined**. Any change to the
86
86
  back to the architecture gate in the product repo — it is never widened from this code repo.
87
87
  ```
88
88
 
89
+ `product-repo` is the one field CI resolves on disk (contract-check, lineage-check, epic-open,
90
+ reconcile-debt all read it). Every gate resolves it the SAME way: an **absolute** path is used as-is; a
91
+ **relative** path is joined to this `link.md`'s own directory, `specs/<story>/` — `../../` climbs out
92
+ of the story dir and out of `specs/` to the code-repo root, so a hub checked out beside the code repo
93
+ is `../../../<hub-dir>`. A relative value that only resolves from the **repo root** still works too
94
+ (what contract-check historically did), so older `link.md` files keep gating. Write it as an
95
+ **unquoted scalar** — `"..."` or `'...'` is taken literally, and a leading `~`/`$VAR` is never
96
+ expanded. An unreachable path degrades the hub-reading gates to a PASS-with-note rather than failing
97
+ them, and each one now says so in its output.
98
+
89
99
  ## Do not re-invent the contract
90
100
 
91
101
  The spec **quotes** the locked surface; it never extends it. To confirm the surface the spec relies on
@@ -93,7 +103,7 @@ matches the lock, run from the **product** repo:
93
103
 
94
104
  ```bash
95
105
  awk '/CONTRACT-SURFACE:BEGIN/{f=1;next} /CONTRACT-SURFACE:END/{f=0} f' \
96
- epics/EP-<slug>/contract.md | shasum -a 256
106
+ epics/EP-<slug>/contract.md | tr -d '\r' | shasum -a 256
97
107
  # compare against epics/EP-<slug>/.sdlc/contract-lock.json
98
108
  ```
99
109