yadflow 3.18.1 → 4.0.0-next.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.
Files changed (156) hide show
  1. package/CHANGELOG.md +355 -0
  2. package/README.md +79 -26
  3. package/bin/commands.mjs +41 -0
  4. package/bin/yad.mjs +437 -124
  5. package/cli/artifact-status.mjs +34 -15
  6. package/cli/checkpoint.mjs +69 -49
  7. package/cli/codeowners-command.mjs +170 -0
  8. package/cli/codeowners.mjs +397 -0
  9. package/cli/commit.mjs +13 -9
  10. package/cli/companion.mjs +2 -2
  11. package/cli/dial.mjs +183 -0
  12. package/cli/docs.mjs +88 -32
  13. package/cli/doctor.mjs +1472 -97
  14. package/cli/epic-state.mjs +3478 -232
  15. package/cli/epic.mjs +506 -0
  16. package/cli/errors.mjs +4 -1
  17. package/cli/gate.mjs +1002 -209
  18. package/cli/history.mjs +556 -0
  19. package/cli/hook.mjs +266 -55
  20. package/cli/hubcommit.mjs +6 -17
  21. package/cli/index-command.mjs +87 -0
  22. package/cli/ledger.mjs +57 -7
  23. package/cli/lib.mjs +184 -18
  24. package/cli/manifest.mjs +367 -56
  25. package/cli/migrate.mjs +726 -53
  26. package/cli/mode.mjs +170 -0
  27. package/cli/next.mjs +349 -90
  28. package/cli/openpr.mjs +191 -39
  29. package/cli/people.mjs +654 -0
  30. package/cli/plan.mjs +417 -132
  31. package/cli/platform.mjs +110 -129
  32. package/cli/product-index.mjs +287 -0
  33. package/cli/protection.mjs +706 -0
  34. package/cli/reconcile.mjs +38 -12
  35. package/cli/repo-publish.mjs +24 -26
  36. package/cli/repo.mjs +23 -14
  37. package/cli/report.mjs +21 -15
  38. package/cli/review.mjs +24 -27
  39. package/cli/riskmap-command.mjs +289 -0
  40. package/cli/riskmap.mjs +373 -0
  41. package/cli/setup.mjs +139 -287
  42. package/cli/ship.mjs +7 -6
  43. package/cli/skill.mjs +180 -0
  44. package/cli/skip.mjs +211 -30
  45. package/cli/thread.mjs +42 -17
  46. package/cli/tidy.mjs +20 -20
  47. package/cli/update-commit.mjs +22 -22
  48. package/cli/usage.mjs +115 -109
  49. package/package.json +3 -3
  50. package/skills/sdlc/config.yaml +166 -87
  51. package/skills/sdlc/module-help.csv +35 -35
  52. package/skills/yad-analysis/SKILL.md +125 -65
  53. package/skills/yad-architecture/SKILL.md +34 -23
  54. package/skills/yad-architecture/references/contract-format.md +10 -8
  55. package/skills/yad-backfill/SKILL.md +14 -8
  56. package/skills/yad-backfill/references/backfill.md +1 -1
  57. package/skills/yad-change/SKILL.md +127 -52
  58. package/skills/yad-change/references/triage.md +42 -28
  59. package/skills/yad-checks/SKILL.md +89 -45
  60. package/skills/yad-checks/references/check-gates.md +315 -92
  61. package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
  62. package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
  63. package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
  64. package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
  65. package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
  66. package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
  67. package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
  68. package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
  69. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
  70. package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
  71. package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
  72. package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
  73. package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
  74. package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
  75. package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
  76. package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
  77. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
  78. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
  79. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
  80. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
  81. package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
  82. package/skills/yad-commit/SKILL.md +6 -6
  83. package/skills/yad-connect-design/SKILL.md +6 -6
  84. package/skills/yad-connect-design/references/design-context.md +1 -1
  85. package/skills/yad-connect-design/references/design-registry.md +2 -2
  86. package/skills/yad-connect-docs/SKILL.md +12 -12
  87. package/skills/yad-connect-docs/references/docs-registry.md +1 -1
  88. package/skills/yad-connect-learning/SKILL.md +5 -5
  89. package/skills/yad-connect-learning/references/learning-registry.md +2 -2
  90. package/skills/yad-connect-repos/SKILL.md +92 -54
  91. package/skills/yad-connect-repos/references/code-context.md +6 -6
  92. package/skills/yad-connect-repos/references/hub-config.md +68 -58
  93. package/skills/yad-connect-repos/references/repos-registry.md +10 -9
  94. package/skills/yad-connect-repos/references/risk-map.md +81 -0
  95. package/skills/yad-connect-testing/SKILL.md +6 -6
  96. package/skills/yad-connect-testing/references/testing-context.md +3 -4
  97. package/skills/yad-connect-testing/references/testing-registry.md +2 -2
  98. package/skills/yad-defects/SKILL.md +8 -8
  99. package/skills/yad-discovery/SKILL.md +130 -94
  100. package/skills/yad-discovery/references/discovery-schema.md +23 -7
  101. package/skills/yad-discovery/references/foundation-schema.md +374 -0
  102. package/skills/yad-docs/SKILL.md +16 -11
  103. package/skills/yad-docs/references/data-mapping.md +9 -7
  104. package/skills/yad-docs/templates/app/package-lock.json +3 -3
  105. package/skills/yad-docs-overview/SKILL.md +32 -17
  106. package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
  107. package/skills/yad-docs-sync/SKILL.md +10 -5
  108. package/skills/yad-docs-sync/references/staleness.md +8 -7
  109. package/skills/yad-engineer-review/SKILL.md +88 -24
  110. package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
  111. package/skills/yad-epic/SKILL.md +178 -100
  112. package/skills/yad-epic/references/state-schema.md +626 -117
  113. package/skills/yad-hub-bridge/SKILL.md +66 -48
  114. package/skills/yad-hub-bridge/references/bridge.md +110 -83
  115. package/skills/yad-hub-bridge/references/login-roster.md +163 -70
  116. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
  117. package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
  118. package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
  119. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
  120. package/skills/yad-implement/SKILL.md +29 -15
  121. package/skills/yad-implement/references/implement-conventions.md +2 -2
  122. package/skills/yad-learn/SKILL.md +9 -9
  123. package/skills/yad-learn/references/learning-state.md +2 -2
  124. package/skills/yad-open-pr/SKILL.md +64 -29
  125. package/skills/yad-pair-review/SKILL.md +18 -16
  126. package/skills/yad-pair-review/references/session-state.md +4 -4
  127. package/skills/yad-pr-template/SKILL.md +48 -27
  128. package/skills/yad-pr-template/references/risk-routing.md +97 -24
  129. package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
  130. package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
  131. package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
  132. package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
  133. package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
  134. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
  135. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
  136. package/skills/yad-reconcile/SKILL.md +3 -3
  137. package/skills/yad-report/SKILL.md +5 -5
  138. package/skills/yad-review-companion/SKILL.md +12 -9
  139. package/skills/yad-review-gate/SKILL.md +198 -79
  140. package/skills/yad-review-gate/references/gating.md +230 -54
  141. package/skills/yad-run/SKILL.md +86 -56
  142. package/skills/yad-run/references/run-loop.md +67 -45
  143. package/skills/yad-ship/SKILL.md +18 -14
  144. package/skills/yad-spec/SKILL.md +31 -17
  145. package/skills/yad-spec/references/spec-handoff.md +17 -5
  146. package/skills/yad-status/SKILL.md +114 -56
  147. package/skills/yad-stories/SKILL.md +42 -27
  148. package/skills/yad-stories/references/story-schema.md +10 -9
  149. package/skills/yad-stub/SKILL.md +59 -48
  150. package/skills/yad-sync-repos/SKILL.md +3 -3
  151. package/skills/yad-test-cases/SKILL.md +37 -30
  152. package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
  153. package/skills/yad-timeline/SKILL.md +8 -7
  154. package/skills/yad-ui/SKILL.md +46 -25
  155. package/cli/roster.mjs +0 -164
  156. package/skills/sdlc/install.sh +0 -68
@@ -1,70 +1,163 @@
1
- # Login roster — schema, resolution, per-repo routing
2
-
3
- The roster lives in `.sdlc/hub.json` (`roster: [...]`) and is the only thing that turns a platform
4
- **login** into an SDLC **name + role** for the ledger. Schema and the no-tokens rule are documented once
5
- in `../../yad-connect-repos/references/hub-config.md`; this file covers how the bridge *uses* it.
6
- It is populated/edited any time with the `yad roster` CLI command (see that reference).
7
-
8
- ## Entry
9
-
10
- ```json
11
- {
12
- "login": "abdelrahmannasr",
13
- "name": "alice",
14
- "email": "alice@example.com",
15
- "roles": { "hub": ["owner", "reviewer"], "backend": ["domain-owner"] }
16
- }
17
- ```
18
-
19
- - `login` — the GitHub/GitLab username whose review/approval is being mapped.
20
- - `name` — the SDLC name written to `approvals.json` / `comments.json` (the same names as `epic.md`
21
- `owner` and `repos.json` `domain_owner`). Keep it stable.
22
- - `email` — the commit email; drives the **committer → login** reverse lookup used to auto-assign PRs.
23
- - `roles` — a **per-scope map** from a scope (`hub`, or a connected repo name) to the roles held there
24
- (`owner` / `reviewer` / `domain-owner`). A person can hold several roles in one scope (owner **and**
25
- reviewer **and** domain-owner at once), and several scopes; a repo gets several owners/reviewers/
26
- domain-owners by being listed in several people's maps.
27
-
28
- **Back-compat (read on all three shapes):** the per-scope object above; a flat array
29
- `"roles": ["owner","reviewer"]` (treated as `hub` roles); and the legacy single `"role": "owner"`
30
- (a `hub` role). The legacy `repos.json` `domain_owner` field is still honored (see Resolution step 2).
31
-
32
- ## Resolution
33
-
34
- 1. **login → name + roles** from the roster. The `hub` roles map straight to records; each touched
35
- domain `R` contributes the roles in `roles[R]` (a `domain-owner` role carries `domain: R`).
36
- 2. **Legacy domain-owner fallback:** if the resolved `name` equals a repo's `domain_owner` in
37
- `repos.json`, and that repo is a **touched domain** for the step under review, the bridge also emits a
38
- `domain-owner` approval scoped to that repo (`domain: <repo>`). One person owning several repos yields
39
- several `domain-owner` records with different `domain` values — exactly what the gate predicate allows.
40
- 3. **Unmapped login → reviewer (flagged).** A login not in the roster maps to `name: <login>`,
41
- `role: reviewer`, with `<!-- unverified login: <login> -->` in the review record. It counts as a
42
- reviewer but is **never** auto-promoted to owner/domain-owner, so a stranger can never satisfy the
43
- owner/domain-owner requirement. The marker prompts a human to add the login to the roster.
44
-
45
- ## Auto-assignee / auto-reviewer on PR/MR open
46
-
47
- When a review PR/MR is opened (hub `yad gate open`, or a code-repo `yad open-pr`):
48
-
49
- - **Assignee = the committer/opener** — resolved from local git identity (`user.email`, then
50
- `user.name`) through the roster (`email`/`name`/`login`). On GitHub an unresolved committer still
51
- self-assigns via `@me`.
52
- - **Reviewers = `reviewer` + `domain-owner`** for the touched scope(s) (`hub` plus every touched
53
- domain for a hub review; the repo itself for a code PR), **minus the committer** — you do not review
54
- your own PR. The artifact **owner/author is recorded, not requested.**
55
- - Logins are validated against the hub during `yad setup` / `yad doctor` (`gh api users/<login>`,
56
- `glab api users?username=<login>`); a miss is flagged `unverified` but never blocks (fail-open).
57
-
58
- ## Per-repo routing (stories review, and any escalated step)
59
-
60
- The stories review needs a `domain-owner` per repo in the **union of every story's `repos`**. On the
61
- review PR the bridge makes this legible and enforceable:
62
-
63
- - Add a `domain:<repo>` **label** per touched repo.
64
- - **Request** each touched repo's `domain_owner` login as a reviewer (resolved via the roster).
65
- - On `sync`, an approval from login *L* maps to `domain-owner` for repo *R* **iff**
66
- `repos.json[R].domain_owner == roster[L].name`. So a domain owner's approval is scoped to exactly the
67
- repos they own, and a repo with no approving owner shows up as still-required in the gate report.
68
-
69
- This is the same touched-domains computation the gate uses (`../yad-review-gate/references/gating.md`):
70
- architecture+contract → `epic.repos`; stories → union of story `repos`.
1
+ # Login attribution — who a record names
2
+
3
+ This file used to describe the reviewer roster; E62 removed the roster, and it now describes how records
4
+ are attributed without one.
5
+
6
+ yadflow keeps **no stored list of people**. The platform already knows who reviewed, and repository
7
+ access already decides who may approve. So every record names the **platform login** that acted.
8
+
9
+ ## Approvals and comments
10
+
11
+ When `yad gate sync` (or CI, `yad gate ci`) reads a review PR/MR:
12
+
13
+ - An approval becomes one `approvals.json` record per person:
14
+ ```json
15
+ { "artifact": "<artifact>", "step": "<step id>", "approver": "<platform login>", "status": "approved",
16
+ "date": "<YYYY-MM-DD>", "source": "bridge", "artifactHash": "<hash>", "approvedAt": "<time>",
17
+ "pr": <n>, "commit": "<sha>", "url": "<review url>", "reviewId": "<id>", "engagement": "verified|none" }
18
+ ```
19
+ `pr` is present when the PR/MR number is known. There is no `role`, no `domain` and no `unverified`
20
+ flag. `commit`, `url` and `reviewId` are the platform's evidence for the review (E64), written only
21
+ when the platform gives them — GitHub gives all three; GitLab gives none of them, because an MR
22
+ approval is not tied to a commit. They are a record of what happened; the gate still decides on the
23
+ login and the fingerprint. `approvedAt` is the time the platform gives: GitHub's submission time, or
24
+ GitLab's `approved_at`. A GitLab instance that does not send `approved_at`, and every GitLab record
25
+ written before E64, hold the date the sync first recorded the approval instead. A date is read as
26
+ "time unknown", never compared with a time.
27
+ - A comment becomes a `comments.json` record:
28
+ `{ artifact, step, commenter: <platform login>, round, count, date }` — no `role`.
29
+ - The dated side file `reviews/<artifact>--<date>--approved.md` lists
30
+ `- <approver> — approved <date> (<source>)`. An older record's role is printed only if that record
31
+ still has one.
32
+
33
+ There is no lookup step: the login the platform reports is the name written. The gate counts distinct
34
+ `approver` values, so the same person is one approver however many records name them.
35
+
36
+ **Local ledger (no platform).** The `yad-review-gate` skill writes the same shapes by hand. `approver` /
37
+ `commenter` is the reviewer's platform login, or the name they give when there is no platform. A team
38
+ gate needs 1 distinct approver (solo mode waives it); nothing checks that it is not the artifact's author, so do not record the
39
+ author's own approval.
40
+
41
+ ## Older records
42
+
43
+ Records written while the roster existed may carry `role` and `domain`, and may name a person by their
44
+ roster `name` instead of their login. The gate never counts a role; the records stay on disk.
45
+
46
+ On the first sync after the upgrade, `yad gate sync` recognises the person such a bridge record names,
47
+ so it continues that record instead of treating the approval as new:
48
+ 0. **An exact submission time comes first (GitHub).** When exactly one older record holds the second a
49
+ review was submitted, that review continues it, whatever name the roster now gives it. It continues the
50
+ records **of that review only** — the ones holding that second — never the whole name: two people an
51
+ older release wrote under one name each keep their own record, and neither reads as the other's newer
52
+ review. A name already continued by a time is not an "orphan" for a later approval to guess at (step 2).
53
+ A match by name is per review too: when some of the name's records hold the approval's own time, it
54
+ continues those. On a closed step, a record under that name that no review continued is kept as
55
+ history. **Known cost (chosen 2026-09-17):** the records cannot tell two people who once shared a name
56
+ from one person whose role records ended up at two different times (older releases moved all of a
57
+ person's records together, so that needs a role dropped on a closed step and a later re-approval, or a
58
+ hand edit). Keeping protects the first, likelier case; in the second, that closed step lists the person
59
+ twice, so `gate status` shows one approver more. Nothing advances on a closed step. The name table
60
+ can be wrong — renaming one of two people who shared a name hands the older records to whoever kept
61
+ it. Older GitLab records hold only a date, so there a rename can still put an older approval on the
62
+ wrong person: **do not rename a shared roster name while reviews with older approvals are open.**
63
+ 1. **With the roster still on disk** — the roster's `name` → `login` pairs make the match exact. This is
64
+ the ONLY thing the roster is read for; it decides nothing about the gate. A name equal to another
65
+ entry's login is still exact (the older record under a name was written from that entry). The one
66
+ exception is a name the roster gives to TWO logins: a record under it could be either person, so it is
67
+ treated like a record no name can place (step 2), and `yad doctor` warns `people:roster-ambiguous`. A
68
+ record an older release marked `unverified` already names a login and is never translated.
69
+ 2. **Without a roster** — a record is continued only when nothing else could be it: the same submission
70
+ time (GitHub), or — on an open step only — exactly one unmatched approval against exactly one
71
+ unmatched older approver of that PR (GitLab, whose older records hold only a date — this rule still
72
+ applies when the approval now carries `approved_at`). People are never matched
73
+ by list order, and the result is the same whatever order the platform lists reviews in.
74
+ 3. The continued record keeps the fingerprint and dates it carried, unless the platform shows a newer
75
+ review (a later submission time — a time on both sides, so a date-only record never reads as older —
76
+ or a different PR/MR). When one person's older records
77
+ disagree, the **stale** one is kept — stale meaning outside the fingerprints the gate accepts for the
78
+ artifact (`acceptedHashes`), not merely different from today's hash.
79
+ A continued record is rewritten under the login and drops `role`, `domain` and `unverified`: they
80
+ count for nothing, and a `role` left on a login-named record would make the next sync read it as an
81
+ older record and look its login up as a roster name — which can be another person's. The dated
82
+ `reviews/<artifact>--<date>--approved.md` keeps the roles as they were.
83
+ 4. **Still ambiguous:** on a closed step nothing is added (its record is history, and a second entry would
84
+ count one person twice); on an open step the approval is recorded against a stale fingerprint from
85
+ those older records, so it must be given again on a new review. If none of those older records is
86
+ stale, there is nothing stale to bind to, and the approval is recorded against the current content.
87
+
88
+ Comment rounds use the same name → login pairs, so the first sync does not open a new round for
89
+ unchanged threads. Without the roster (or for a name two logins share) that sync may open one new round.
90
+
91
+ ## Recording the login on older records (E64)
92
+
93
+ The steps above need the roster. So, while it is still on disk, every sync write (`yad gate sync`,
94
+ `yad gate ci`) records the login on each older record the roster can place for certain. After that, only
95
+ the records it could not place (below) still need the roster.
96
+
97
+ - **What changes on a record.** `approver` / `commenter` becomes the login, and `role`, `domain` and
98
+ `unverified` are removed, exactly as step 3 rewrites a continued record. An approval keeps the name it
99
+ had in `rosterName`. Everything else — fingerprint, dates, `pr` — stays as it was. The dated
100
+ `reviews/*--approved.md` keeps the roles.
101
+ - **One record per person, but only for one review.** An older release wrote one record per role, so one
102
+ approval can be several records. They become one record — keeping the stale fingerprint when they
103
+ disagree — only when they prove they are one review: every one holds the same submission **time** and
104
+ the same `pr`. A date is not proof. Every GitLab record from before E64 holds the day it was synced, so
105
+ two people a renamed roster now gives one name look the same; and a hand-written record holds no time
106
+ at all. Such records are left as they are. On GitLab this means one person's role records usually stay
107
+ as they are, and the roster keeps matching them on the next sync of their review.
108
+ - **Also left as they are:** a record marked `unverified` (it names a login already), a record under a
109
+ name two logins share, a name the roster does not hold, and a comment round in which two records would
110
+ then name the same login.
111
+ - **The exact time still wins.** An approval stamped onto the wrong login (a renamed shared name) keeps
112
+ `rosterName`, so step 0 still matches it by its submission time and hands it to the right person.
113
+ - **When it runs.** `yad gate sync` stamps every step of the epic it writes, closed steps included. On a
114
+ verified Product, `yad gate ci` does it on every merge run — the merge event and the scheduled reconcile
115
+ that re-runs it — for **every** epic, including one with no open review, and commits it with the rest.
116
+ It never writes before a merge; it skips an epic with uncommitted changes under `.sdlc` or `reviews`;
117
+ a run whose sync fails leaves no stamp behind; and a record it cannot read, or an epic whose stamp fails, is left as it
118
+ is and named, in any epic — the merged review's own included — without stopping that merge. `yad gate sync <epic>` writes the stamp even when the epic
119
+ has no review PR on file. A Product with no platform never needs it: nothing there matches logins.
120
+ - **When the roster can go.** `yad doctor`'s `people:roster-unused` says so: how many older records still
121
+ wait for a gate write (and in which epics), or that none do. Records it cannot place (a shared name,
122
+ records that disagree about the review) are then matched by submission time or given again on a new
123
+ review.
124
+
125
+ ## Who wrote a record (`by`)
126
+
127
+ Skip, defer, closing records, kill-switch flips and mode changes carry a `by`. It is:
128
+
129
+ 1. the login `gh api user` (GitHub, asked on the repo's own host) or `glab api user` (GitLab) reports, else
130
+ 2. git `user.name`, else `null`.
131
+
132
+ `YAD_PLATFORM_LOGIN=0` turns the platform lookup off (offline work, tests). The lookup is best effort and
133
+ never blocks the command. Checkpoint and tidy commit subjects show `@<login>` when the login is known,
134
+ else the git name.
135
+
136
+ ## Opening a PR/MR
137
+
138
+ `yad gate open` (review PRs) and `yad open-pr` (task PRs):
139
+
140
+ - **Assignee** = the person opening it: `@me` on GitHub (`gh` resolves it), the login `glab api user`
141
+ reports on GitLab (no assignee when that lookup fails).
142
+ - **Reviewers: none are requested.** The command prints
143
+ `no reviewers were requested — ask them on the PR itself`. On a code-repo PR, `yad open-pr` then
144
+ prints a reviewer suggestion from recent history and CODEOWNERS (E68) — a hint, never a request.
145
+ - **Labels (`yad gate open` only):** a `domain:<repo>` label per touched domain is still applied — for
146
+ `stories-review` the union of every story's `repos`, for a step tagged `contract`, `auth` or
147
+ `payments` the epic's `repos`. The labels are a hint for whom to ask; they add no approvals.
148
+ `yad open-pr` adds no labels.
149
+
150
+ ## Legacy data `yad doctor` reports
151
+
152
+ None of these decides anything any more. `yad doctor` warns so the team can delete them:
153
+
154
+ | Warning | What is left on disk |
155
+ |---|---|
156
+ | `people:roster-unused` | a non-empty `roster` in `.sdlc/hub.json` — still read for its name → login pairs only (see "Older records"); the hint counts the older records still waiting for a gate write to record their login, and says when it is safe to delete |
157
+ | `people:roster-ambiguous` | a roster name given to more than one login (only checked when the roster is non-empty) |
158
+ | `people:domain-owners-unused` | a repo in `.sdlc/repos.json` that lists `domain_owner` / `domain_owners` |
159
+ | `people:verified-authors-unused` | a `verified_authors` list in `.sdlc/hub.json`, or a `.sdlc/verified-authors` file in the Product or a connected repo |
160
+ | `people:allowlist-gate-stale` | an older `checks/verified-commits.sh` that still enforces the author list (a local-ledger Product's CI files are not refreshed by `yad check --fix`) |
161
+
162
+ `yad roster` is removed (typing it prints a notice and exits 1), and `yad setup` collects no reviewers,
163
+ roles, repo owners or commit emails. The verified-commits gate checks platform-Verified signatures only.
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env bash
2
- # Hub review routing — the front-half analogue of yad-pr-template's risk-route.sh. Reads a hub review
3
- # PR/MR description's "Impact & Risk (front-half)" block and prints the required reviewers, reusing
4
- # yad-review-gate's rule: base = owner + 1 reviewer; if a risk tag (contract|auth|payments) is set OR
5
- # the artifact is the stories set, ALSO a domain-owner per touched repo. Advisory: it ROUTES the human
2
+ # Product review routing — the Shape analogue of yad-pr-template's risk-route.sh. Reads a Product review
3
+ # PR/MR description's "Impact & Risk (front-half)" block and prints how many approvers the gate asks for,
4
+ # as the same sum the gate prints: base 1, plus a risk step from the tags — `contract` +2, `auth` or
5
+ # `payments` +1, the largest and never the sum. Only the base holds the gate; the risk step is advisory.
6
+ # The gate reports that count capped at the number of active people less one (E72); this
7
+ # script cannot count people, so it prints the full sum. There are no roles and no named owners: yadflow
8
+ # keeps no list of people (E62), so when a risk tag raises the count the touched repos are printed as a
9
+ # hint for whom to ask (with no risk step it prints the base line alone). Advisory: it ROUTES the human
6
10
  # review; it does not approve or merge.
7
11
  set -euo pipefail
8
12
 
@@ -18,33 +22,32 @@ value_of() {
18
22
 
19
23
  risk_tags="$(printf '%s' "$(value_of 'Risk tags:')" | tr 'A-Z' 'a-z')"
20
24
  repos="$(value_of 'Domains.*touched:')"
21
- artifact="$(value_of 'Artifact:')"
22
25
 
23
26
  echo "Risk tags: ${risk_tags:-none}"
24
27
  echo "Repos touched: ${repos:-unspecified}"
25
28
 
26
- escalate=no
29
+ step=0
30
+ tier=""
27
31
  why=""
28
- case "$risk_tags" in
29
- *contract*) escalate=yes; why="risk tag: contract" ;;
30
- esac
31
- case "$risk_tags" in *auth*) escalate=yes; why="${why:+$why, }risk tag: auth" ;; esac
32
- case "$risk_tags" in *payments*) escalate=yes; why="${why:+$why, }risk tag: payments" ;; esac
33
- # The stories review routes per-repo even with no risk tag.
34
- case "$artifact" in *stories*) escalate=yes; why="${why:+$why, }stories per-repo routing" ;; esac
32
+ # Whole tags only, as the gate reads them (`gateRuleFor`): `oauth` or `contracts` is not a risk tag.
33
+ tags=" $(printf '%s' "$risk_tags" | tr ',;' ' ' | tr -s '[:space:]' ' ') "
34
+ case "$tags" in *" auth "*) step=1; tier=high; why="risk tag: auth" ;; esac
35
+ case "$tags" in *" payments "*) step=1; tier=high; why="${why:+$why, }risk tag: payments" ;; esac
36
+ case "$tags" in *" contract "*) step=2; tier=contract; why="${why:+$why, }risk tag: contract" ;; esac
35
37
 
36
- if [ "$escalate" = "yes" ]; then
37
- echo "ROUTE: ESCALATED (${why}) -> owner + 1 reviewer PLUS one domain-owner approval per touched repo"
38
- echo " (same escalation as yad-review-gate; map each repo to its domain_owner in repos.json)."
38
+ if [ "$step" -gt 0 ]; then
39
+ echo "ROUTE: $((1 + step)) approvers = base 1 + ${tier} risk ${step} (${why})"
40
+ echo " Only the base holds the gate: 1 approval (the platform decides whether the author may give it)."
41
+ echo " The risk step is advisory; \`yad gate status\` prints it capped by the active people. Ask reviewers who know the touched repos:"
39
42
  case "$repos" in
40
43
  ""|*"<"*|*"…"*|*"|"*)
41
- echo " (Repos line not filled in — list each touched repo to route the domain owners.)" ;;
44
+ echo " (Repos line not filled in — list each touched repo so the right reviewers can be asked.)" ;;
42
45
  *)
43
46
  printf '%s\n' "$repos" | tr ',' '\n' | while IFS= read -r r; do
44
47
  r="$(printf '%s' "$r" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//')"
45
- [ -n "$r" ] && echo " - domain-owner: $r"
48
+ if [ -n "$r" ]; then echo " - $r"; fi # not `&&`: an empty last item (`a, b,`) would exit 1
46
49
  done ;;
47
50
  esac
48
51
  else
49
- echo "ROUTE: base rule -> owner + 1 reviewer (no domain-owner escalation)."
52
+ echo "ROUTE: 1 approver = base 1 (no risk step)."
50
53
  fi
@@ -1,5 +1,5 @@
1
1
  # yad-managed: yad-hub-bridge
2
- # Merge-time gate sync for the PRODUCT HUB (Path B). CI is the SOLE writer of the ledger, and it
2
+ # Merge-time gate sync for the PRODUCT (Path B). CI is the SOLE writer of the ledger, and it
3
3
  # writes ONLY at merge, ONLY to the default branch.
4
4
  #
5
5
  # During review there is NO CI write: the platform PR is the source of truth (native approvals +
@@ -24,7 +24,7 @@
24
24
  # last part is load-bearing: before the #163 fix the re-sync re-appended each step's approvals at the
25
25
  # tail, so this job rotated approvals.json and committed the reorder every 15 minutes, forever (issue
26
26
  # #163). These jobs run an EXACT version resolved from the repo (see the resolver in each job), so a
27
- # hub sitting on a pre-#163 yadflow keeps that behaviour until someone upgrades it — if the resolved
27
+ # Product sitting on a pre-#163 yadflow keeps that behaviour until someone upgrades it — if the resolved
28
28
  # pin is older than 3.15.3, run `yad update` or disable the schedule.
29
29
  # On GitHub a scheduled workflow runs automatically once committed (no setup).
30
30
  #
@@ -48,13 +48,13 @@ permissions:
48
48
 
49
49
  env:
50
50
  # The manual override for which yadflow these jobs run: set a repository (or organization) Actions
51
- # **variable** named YAD_VERSION to e.g. `3.15.2`, under Settings → Secrets and variables → Actions →
51
+ # **variable** named YAD_VERSION to e.g. `4.0.0`, under Settings → Secrets and variables → Actions →
52
52
  # Variables. It lives there and not in this file, which `yad` owns and rewrites byte-for-byte from
53
53
  # its template on every sync — a version edited in here would be silently reverted.
54
54
  #
55
55
  # Left EMPTY the jobs resolve an exact pin from the repo itself instead of floating on the major (see
56
56
  # the resolver step in each job). Floating meant a release could change what a scheduled job does with
57
- # nobody deciding to upgrade — the amplification path in issue #163, where 3.13.1 took one hub from 20
57
+ # nobody deciding to upgrade — the amplification path in issue #163, where 3.13.1 took one Product from 20
58
58
  # to 96 churn commits an hour.
59
59
  YAD_VERSION: ${{ vars.YAD_VERSION || '' }}
60
60
 
@@ -87,9 +87,9 @@ jobs:
87
87
  # >>> yad-pin
88
88
  # Which yadflow this job runs, most explicit source first:
89
89
  # 1. $YAD_VERSION — the platform variable, a human's deliberate override, verbatim
90
- # 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this hub
90
+ # 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this Product
91
91
  # 3. .sdlc/cli-version.json — the version that last wired it (`yad update` re-stamps this)
92
- # 4. 3 — floating major, only when nothing above resolves
92
+ # 4. $YAD_MAJOR — floating major, only when nothing above resolves
93
93
  # Sources 2 and 3 are machine-written files a stale or hand-edited checkout can get wrong, so
94
94
  # they are VALIDATED: an exact release token of the major this fragment was written for. That
95
95
  # also keeps the value safe to interpolate into `npx -p "yadflow@$V"` — a repo-controlled
@@ -98,20 +98,30 @@ jobs:
98
98
  # Pipes, not bash here-strings, so the block stays byte-identical to the GitLab fragment's
99
99
  # (which must run under `sh` when an image has no bash). `sed` drains its input, so nothing
100
100
  # SIGPIPEs `tr`.
101
+ # The major this fragment ships with. ONE literal, read by the check, the message and the fallback
102
+ # below, so they cannot drift apart; scripts/pin-major-check.sh holds it to the major a release
103
+ # publishes. `yad update` rewrites this file and re-stamps .sdlc/cli-version.json together, so a
104
+ # Product moves both to the new major at once — and a pin from an older major is skipped.
105
+ YAD_MAJOR=4
101
106
  yad_pin_read() { # <file> <key>
102
107
  [ -f "$1" ] || return 0
103
108
  tr -d '\n' < "$1" 2>/dev/null | sed -nE "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p"
104
109
  }
105
- yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq '^3\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; }
110
+ yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq "^${YAD_MAJOR}\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\$"; }
106
111
  YAD_PKG="${YAD_VERSION:-}"
107
112
  if [ -z "$YAD_PKG" ]; then
108
113
  for _cand in "$(yad_pin_read .sdlc/hub.json gate_sync_version)" "$(yad_pin_read .sdlc/cli-version.json version)"; do
109
114
  [ -n "$_cand" ] || continue
110
115
  if yad_pin_valid "$_cand"; then YAD_PKG="$_cand"; break; fi
111
- echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact 3.x release." >&2
116
+ echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact ${YAD_MAJOR}.x release." >&2
112
117
  done
113
118
  fi
114
- YAD_PKG="${YAD_PKG:-3}"
119
+ if [ -z "$YAD_PKG" ]; then
120
+ YAD_PKG="$YAD_MAJOR"
121
+ # A bare major is an npm RANGE, and a range never matches a prerelease: while only ${YAD_MAJOR}.0.0-next.N
122
+ # exists, npm fails with ETARGET. Say what to do before that happens.
123
+ echo "yad-gate-sync: no exact pin resolved — floating on yadflow@${YAD_MAJOR}. If npm cannot find it, run: yad update — and commit .sdlc/cli-version.json — or set YAD_VERSION." >&2
124
+ fi
115
125
  echo "yad-gate-sync: running yadflow@${YAD_PKG}"
116
126
  # <<< yad-pin
117
127
  echo "YAD_PKG=${YAD_PKG}" >> "$GITHUB_ENV"
@@ -155,9 +165,9 @@ jobs:
155
165
  # >>> yad-pin
156
166
  # Which yadflow this job runs, most explicit source first:
157
167
  # 1. $YAD_VERSION — the platform variable, a human's deliberate override, verbatim
158
- # 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this hub
168
+ # 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this Product
159
169
  # 3. .sdlc/cli-version.json — the version that last wired it (`yad update` re-stamps this)
160
- # 4. 3 — floating major, only when nothing above resolves
170
+ # 4. $YAD_MAJOR — floating major, only when nothing above resolves
161
171
  # Sources 2 and 3 are machine-written files a stale or hand-edited checkout can get wrong, so
162
172
  # they are VALIDATED: an exact release token of the major this fragment was written for. That
163
173
  # also keeps the value safe to interpolate into `npx -p "yadflow@$V"` — a repo-controlled
@@ -166,20 +176,30 @@ jobs:
166
176
  # Pipes, not bash here-strings, so the block stays byte-identical to the GitLab fragment's
167
177
  # (which must run under `sh` when an image has no bash). `sed` drains its input, so nothing
168
178
  # SIGPIPEs `tr`.
179
+ # The major this fragment ships with. ONE literal, read by the check, the message and the fallback
180
+ # below, so they cannot drift apart; scripts/pin-major-check.sh holds it to the major a release
181
+ # publishes. `yad update` rewrites this file and re-stamps .sdlc/cli-version.json together, so a
182
+ # Product moves both to the new major at once — and a pin from an older major is skipped.
183
+ YAD_MAJOR=4
169
184
  yad_pin_read() { # <file> <key>
170
185
  [ -f "$1" ] || return 0
171
186
  tr -d '\n' < "$1" 2>/dev/null | sed -nE "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p"
172
187
  }
173
- yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq '^3\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; }
188
+ yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq "^${YAD_MAJOR}\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\$"; }
174
189
  YAD_PKG="${YAD_VERSION:-}"
175
190
  if [ -z "$YAD_PKG" ]; then
176
191
  for _cand in "$(yad_pin_read .sdlc/hub.json gate_sync_version)" "$(yad_pin_read .sdlc/cli-version.json version)"; do
177
192
  [ -n "$_cand" ] || continue
178
193
  if yad_pin_valid "$_cand"; then YAD_PKG="$_cand"; break; fi
179
- echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact 3.x release." >&2
194
+ echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact ${YAD_MAJOR}.x release." >&2
180
195
  done
181
196
  fi
182
- YAD_PKG="${YAD_PKG:-3}"
197
+ if [ -z "$YAD_PKG" ]; then
198
+ YAD_PKG="$YAD_MAJOR"
199
+ # A bare major is an npm RANGE, and a range never matches a prerelease: while only ${YAD_MAJOR}.0.0-next.N
200
+ # exists, npm fails with ETARGET. Say what to do before that happens.
201
+ echo "yad-gate-sync: no exact pin resolved — floating on yadflow@${YAD_MAJOR}. If npm cannot find it, run: yad update — and commit .sdlc/cli-version.json — or set YAD_VERSION." >&2
202
+ fi
183
203
  echo "yad-gate-sync: running yadflow@${YAD_PKG}"
184
204
  # <<< yad-pin
185
205
  echo "YAD_PKG=${YAD_PKG}" >> "$GITHUB_ENV"
@@ -1,5 +1,5 @@
1
- # SDLC gate sync — minimal root pipeline for the PRODUCT HUB.
2
- # Written by the yad-hub-bridge `wire` action ONLY when the hub has no existing root
1
+ # SDLC gate sync — minimal root pipeline for the PRODUCT.
2
+ # Written by the yad-hub-bridge `wire` action ONLY when the Product has no existing root
3
3
  # `.gitlab-ci.yml`. The job itself lives in the included fragment so the same single source is
4
4
  # reused whether or not a root pipeline already exists. If a root later grows real jobs, they
5
5
  # coexist with the included yad-* job (which carries `needs: []` and no `stage:`).
@@ -1,6 +1,6 @@
1
1
  # yad-managed-include: yad-hub-bridge
2
- # Merge-time gate sync for the PRODUCT HUB, as an INCLUDABLE fragment (Path B). Pulled into the
3
- # hub's root .gitlab-ci.yml via:
2
+ # Merge-time gate sync for the PRODUCT, as an INCLUDABLE fragment (Path B). Pulled into the
3
+ # Product's root .gitlab-ci.yml via:
4
4
  # include:
5
5
  # - local: '.gitlab/ci/yad-gate-sync.yml'
6
6
  # so wiring never edits the foreign root pipeline beyond that one include line. The job carries
@@ -34,16 +34,16 @@
34
34
  # default branch — the merge advance pushes there) and store it as a masked CI/CD variable
35
35
  # SDLC_GATE_TOKEN. Without it the job fails visibly; recover by setting the token, then re-run the
36
36
  # pipeline or run `yad gate ci --branch <review-branch> --pr <iid> --merged` locally on the default
37
- # branch (advisory `yad gate sync` is read-only in bridge mode and cannot recover a stuck gate).
37
+ # branch (advisory `yad gate sync` is read-only in verified mode and cannot recover a stuck gate).
38
38
  #
39
39
  # Which yadflow this job runs: an EXACT version resolved from the repo at run time — see the `yad-pin`
40
40
  # block in the script below for the precedence. It no longer floats on the major, because floating let a
41
41
  # release change what a scheduled job does with nobody deciding to upgrade (issue #163, where 3.13.1
42
- # took one hub from 20 to 96 churn commits an hour). The trade-off is deliberate and cuts both ways: a
43
- # hub is no longer carried onto a fix automatically, so if the resolved pin is older than 3.15.3, run
42
+ # took one Product from 20 to 96 churn commits an hour). The trade-off is deliberate and cuts both ways: a
43
+ # Product is no longer carried onto a fix automatically, so if the resolved pin is older than 3.15.3, run
44
44
  # `yad update` or disable this schedule.
45
45
  #
46
- # To override it, set a CI/CD variable YAD_VERSION (e.g. `3.15.2`) in project Settings → CI/CD →
46
+ # To override it, set a CI/CD variable YAD_VERSION (e.g. `4.0.0`) in project Settings → CI/CD →
47
47
  # Variables — the same place SDLC_GATE_TOKEN lives, and deliberately NOT in this file, which `yad` owns
48
48
  # and rewrites byte-for-byte from its template on every sync.
49
49
  variables:
@@ -90,9 +90,9 @@ yad-gate-sync:
90
90
  # >>> yad-pin
91
91
  # Which yadflow this job runs, most explicit source first:
92
92
  # 1. $YAD_VERSION — the CI/CD variable, a human's deliberate override, verbatim
93
- # 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this hub
93
+ # 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this Product
94
94
  # 3. .sdlc/cli-version.json — the version that last wired it (`yad update` re-stamps this)
95
- # 4. 3 — floating major, only when nothing above resolves
95
+ # 4. $YAD_MAJOR — floating major, only when nothing above resolves
96
96
  # Sources 2 and 3 are machine-written files a stale or hand-edited checkout can get wrong, so
97
97
  # they are VALIDATED: an exact release token of the major this fragment was written for. That
98
98
  # also keeps the value safe to interpolate into `npx -p "yadflow@$V"` — a repo-controlled
@@ -101,20 +101,30 @@ yad-gate-sync:
101
101
  # the same shell, so YAD_PKG carries into the block below.
102
102
  # Pipes, not bash here-strings: a GitLab runner falls back to `sh` when the image has no bash,
103
103
  # and `<<<` is a bashism. `sed` drains its input (no early exit, so nothing SIGPIPEs `tr`).
104
+ # The major this fragment ships with. ONE literal, read by the check, the message and the fallback
105
+ # below, so they cannot drift apart; scripts/pin-major-check.sh holds it to the major a release
106
+ # publishes. `yad update` rewrites this file and re-stamps .sdlc/cli-version.json together, so a
107
+ # Product moves both to the new major at once — and a pin from an older major is skipped.
108
+ YAD_MAJOR=4
104
109
  yad_pin_read() { # <file> <key>
105
110
  [ -f "$1" ] || return 0
106
111
  tr -d '\n' < "$1" 2>/dev/null | sed -nE "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p"
107
112
  }
108
- yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq '^3\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; }
113
+ yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq "^${YAD_MAJOR}\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\$"; }
109
114
  YAD_PKG="${YAD_VERSION:-}"
110
115
  if [ -z "$YAD_PKG" ]; then
111
116
  for _cand in "$(yad_pin_read .sdlc/hub.json gate_sync_version)" "$(yad_pin_read .sdlc/cli-version.json version)"; do
112
117
  [ -n "$_cand" ] || continue
113
118
  if yad_pin_valid "$_cand"; then YAD_PKG="$_cand"; break; fi
114
- echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact 3.x release." >&2
119
+ echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact ${YAD_MAJOR}.x release." >&2
115
120
  done
116
121
  fi
117
- YAD_PKG="${YAD_PKG:-3}"
122
+ if [ -z "$YAD_PKG" ]; then
123
+ YAD_PKG="$YAD_MAJOR"
124
+ # A bare major is an npm RANGE, and a range never matches a prerelease: while only ${YAD_MAJOR}.0.0-next.N
125
+ # exists, npm fails with ETARGET. Say what to do before that happens.
126
+ echo "yad-gate-sync: no exact pin resolved — floating on yadflow@${YAD_MAJOR}. If npm cannot find it, run: yad update — and commit .sdlc/cli-version.json — or set YAD_VERSION." >&2
127
+ fi
118
128
  echo "yad-gate-sync: running yadflow@${YAD_PKG}"
119
129
  # <<< yad-pin
120
130
  - |
@@ -126,7 +136,7 @@ yad-gate-sync:
126
136
  # unchanged approval record produces an unchanged file and nothing is committed. That last part
127
137
  # is load-bearing — before the #163 fix the re-sync re-appended each step's approvals at the
128
138
  # tail, so this loop rotated approvals.json and committed the reorder on every pass, forever
129
- # (issue #163). This job runs an exact pin resolved from the repo, so a hub still sitting on a
139
+ # (issue #163). This job runs an exact pin resolved from the repo, so a Product still sitting on a
130
140
  # pre-3.15.3 yadflow keeps that behaviour — upgrade it or disable this schedule.
131
141
  # A stuck review MR (a squash merge whose commit dropped the branch name, or a failed merge
132
142
  # push) is always RECENT, so sweep a generous recent window and PAGINATE it fully (--paginate)