yadflow 3.19.0-next.2 → 4.0.0-next.3

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 (158) hide show
  1. package/CHANGELOG.md +371 -0
  2. package/README.md +160 -22
  3. package/bin/commands.mjs +46 -0
  4. package/bin/yad.mjs +506 -117
  5. package/cli/artifact-status.mjs +30 -11
  6. package/cli/capture.mjs +417 -0
  7. package/cli/checkpoint.mjs +47 -27
  8. package/cli/claims.mjs +165 -0
  9. package/cli/codeowners-command.mjs +170 -0
  10. package/cli/codeowners.mjs +397 -0
  11. package/cli/commit.mjs +13 -9
  12. package/cli/dial.mjs +183 -0
  13. package/cli/docs.mjs +88 -32
  14. package/cli/doctor.mjs +1682 -90
  15. package/cli/epic-state.mjs +3460 -211
  16. package/cli/epic.mjs +506 -0
  17. package/cli/errors.mjs +3 -0
  18. package/cli/fold.mjs +247 -0
  19. package/cli/gate-local.mjs +296 -0
  20. package/cli/gate.mjs +977 -183
  21. package/cli/history.mjs +556 -0
  22. package/cli/hook.mjs +264 -53
  23. package/cli/hubcommit.mjs +9 -19
  24. package/cli/index-command.mjs +87 -0
  25. package/cli/ledger.mjs +54 -4
  26. package/cli/lib.mjs +161 -9
  27. package/cli/manifest.mjs +376 -38
  28. package/cli/migrate.mjs +683 -52
  29. package/cli/mode.mjs +170 -0
  30. package/cli/next.mjs +365 -86
  31. package/cli/openpr.mjs +193 -34
  32. package/cli/owners.mjs +266 -0
  33. package/cli/people.mjs +657 -0
  34. package/cli/plan.mjs +466 -125
  35. package/cli/platform.mjs +109 -128
  36. package/cli/product-index.mjs +287 -0
  37. package/cli/protection.mjs +706 -0
  38. package/cli/reconcile.mjs +40 -12
  39. package/cli/repo-publish.mjs +24 -26
  40. package/cli/repo.mjs +23 -14
  41. package/cli/report.mjs +24 -14
  42. package/cli/review.mjs +19 -22
  43. package/cli/riskmap-command.mjs +289 -0
  44. package/cli/riskmap.mjs +373 -0
  45. package/cli/setup.mjs +127 -279
  46. package/cli/ship.mjs +6 -5
  47. package/cli/skill.mjs +180 -0
  48. package/cli/skip.mjs +211 -30
  49. package/cli/thread.mjs +41 -16
  50. package/cli/tidy.mjs +18 -18
  51. package/cli/update-commit.mjs +21 -21
  52. package/cli/usage.mjs +115 -109
  53. package/package.json +3 -3
  54. package/skills/sdlc/config.yaml +149 -70
  55. package/skills/sdlc/module-help.csv +24 -24
  56. package/skills/yad-analysis/SKILL.md +121 -59
  57. package/skills/yad-architecture/SKILL.md +30 -18
  58. package/skills/yad-architecture/references/contract-format.md +8 -5
  59. package/skills/yad-backfill/SKILL.md +10 -4
  60. package/skills/yad-backfill/references/backfill.md +8 -1
  61. package/skills/yad-backfill/templates/checks/backfill-check.sh +10 -2
  62. package/skills/yad-change/SKILL.md +119 -42
  63. package/skills/yad-change/references/triage.md +42 -27
  64. package/skills/yad-checks/SKILL.md +68 -35
  65. package/skills/yad-checks/references/check-gates.md +310 -85
  66. package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
  67. package/skills/yad-checks/templates/checks/contract-check.sh +130 -9
  68. package/skills/yad-checks/templates/checks/epic-open.sh +2 -2
  69. package/skills/yad-checks/templates/checks/ledger-guard.sh +55 -8
  70. package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
  71. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +1 -1
  72. package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
  73. package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
  74. package/skills/yad-checks/templates/github/yad-checks.yml +13 -2
  75. package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
  76. package/skills/yad-checks/templates/github/yad-update-guard.yml +7 -5
  77. package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
  78. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +10 -1
  79. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
  80. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +3 -3
  81. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
  82. package/skills/yad-checks/templates/hooks/ledger-guard.sh +37 -6
  83. package/skills/yad-checks/templates/hooks/yad-capture.sh +53 -0
  84. package/skills/yad-commit/SKILL.md +4 -4
  85. package/skills/yad-connect-design/SKILL.md +5 -5
  86. package/skills/yad-connect-design/references/design-context.md +1 -1
  87. package/skills/yad-connect-design/references/design-registry.md +2 -2
  88. package/skills/yad-connect-docs/SKILL.md +11 -11
  89. package/skills/yad-connect-docs/references/docs-registry.md +1 -1
  90. package/skills/yad-connect-learning/SKILL.md +5 -5
  91. package/skills/yad-connect-learning/references/learning-registry.md +2 -2
  92. package/skills/yad-connect-repos/SKILL.md +66 -45
  93. package/skills/yad-connect-repos/references/code-context.md +4 -4
  94. package/skills/yad-connect-repos/references/hub-config.md +47 -51
  95. package/skills/yad-connect-repos/references/repos-registry.md +8 -7
  96. package/skills/yad-connect-repos/references/risk-map.md +81 -0
  97. package/skills/yad-connect-testing/SKILL.md +5 -5
  98. package/skills/yad-connect-testing/references/testing-context.md +3 -4
  99. package/skills/yad-connect-testing/references/testing-registry.md +2 -2
  100. package/skills/yad-defects/SKILL.md +7 -7
  101. package/skills/yad-discovery/SKILL.md +131 -94
  102. package/skills/yad-discovery/references/discovery-schema.md +22 -6
  103. package/skills/yad-discovery/references/foundation-schema.md +374 -0
  104. package/skills/yad-docs/SKILL.md +13 -8
  105. package/skills/yad-docs/references/data-mapping.md +9 -7
  106. package/skills/yad-docs/templates/app/package-lock.json +3 -3
  107. package/skills/yad-docs-overview/SKILL.md +30 -15
  108. package/skills/yad-docs-overview/references/pipeline-model.md +33 -20
  109. package/skills/yad-docs-sync/SKILL.md +10 -5
  110. package/skills/yad-docs-sync/references/staleness.md +8 -7
  111. package/skills/yad-engineer-review/SKILL.md +81 -17
  112. package/skills/yad-engineer-review/references/ship-and-record.md +17 -8
  113. package/skills/yad-epic/SKILL.md +173 -92
  114. package/skills/yad-epic/references/state-schema.md +610 -94
  115. package/skills/yad-hub-bridge/SKILL.md +57 -38
  116. package/skills/yad-hub-bridge/references/bridge.md +94 -67
  117. package/skills/yad-hub-bridge/references/login-roster.md +163 -70
  118. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
  119. package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
  120. package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
  121. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +21 -11
  122. package/skills/yad-implement/SKILL.md +27 -13
  123. package/skills/yad-implement/references/implement-conventions.md +2 -2
  124. package/skills/yad-learn/SKILL.md +9 -9
  125. package/skills/yad-learn/references/learning-state.md +2 -2
  126. package/skills/yad-open-pr/SKILL.md +61 -26
  127. package/skills/yad-pair-review/SKILL.md +7 -5
  128. package/skills/yad-pair-review/references/session-state.md +1 -1
  129. package/skills/yad-pr-template/SKILL.md +47 -26
  130. package/skills/yad-pr-template/references/risk-routing.md +97 -24
  131. package/skills/yad-pr-template/templates/checks/pr-template.sh +34 -11
  132. package/skills/yad-pr-template/templates/checks/pr-title.sh +32 -9
  133. package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
  134. package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
  135. package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
  136. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +14 -13
  137. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +14 -12
  138. package/skills/yad-reconcile/SKILL.md +3 -3
  139. package/skills/yad-report/SKILL.md +5 -5
  140. package/skills/yad-review-companion/SKILL.md +6 -3
  141. package/skills/yad-review-gate/SKILL.md +203 -93
  142. package/skills/yad-review-gate/references/gating.md +232 -54
  143. package/skills/yad-run/SKILL.md +81 -51
  144. package/skills/yad-run/references/run-loop.md +61 -39
  145. package/skills/yad-ship/SKILL.md +15 -11
  146. package/skills/yad-spec/SKILL.md +21 -6
  147. package/skills/yad-spec/references/spec-handoff.md +17 -5
  148. package/skills/yad-status/SKILL.md +115 -50
  149. package/skills/yad-stories/SKILL.md +37 -21
  150. package/skills/yad-stories/references/story-schema.md +7 -6
  151. package/skills/yad-stub/SKILL.md +58 -47
  152. package/skills/yad-sync-repos/SKILL.md +2 -2
  153. package/skills/yad-test-cases/SKILL.md +32 -23
  154. package/skills/yad-test-cases/references/test-cases-schema.md +7 -4
  155. package/skills/yad-timeline/SKILL.md +8 -7
  156. package/skills/yad-ui/SKILL.md +41 -18
  157. package/cli/roster.mjs +0 -164
  158. package/skills/sdlc/install.sh +0 -68
@@ -1,8 +1,8 @@
1
1
  # Impact & Risk block and review routing
2
2
 
3
3
  The PR/MR template (Phase 3 build plan §D) carries an **Impact & Risk** block so every change states
4
- its blast radius before review, and so high-risk changes pull in the right reviewers automatically —
5
- reusing the escalation `yad-review-gate` already applies on the Shape gates.
4
+ its blast radius before review, and so a high-risk change shows how many approvers it asks for —
5
+ the same count `yad-review-gate` prints on the Shape gates.
6
6
 
7
7
  ## The Impact & Risk block
8
8
 
@@ -14,41 +14,114 @@ reusing the escalation `yad-review-gate` already applies on the Shape gates.
14
14
  - **Rollback plan:** <how to revert>
15
15
  ```
16
16
 
17
- - **Domains / repos touched** — the domains this change affects; these are the candidate domain owners
18
- when the change escalates.
17
+ - **Domains / repos touched** — the domains this change affects. When the count is raised,
18
+ `risk-route.sh` lists them as a hint for whom to ask. yadflow keeps no list of people, so no names come from it.
19
19
  - **Contract surface touched** — `yes` means the diff changes the shared contract surface. That path is
20
- governed by `contract-check` (needs `Contract-Change: yes` + a re-locked contract) AND it escalates
21
- the review (contract is a `yad-review-gate` risk tag).
20
+ governed by `contract-check` (needs `Contract-Change: yes` + a re-locked contract) AND it raises the
21
+ approver count (contract is a `yad-review-gate` risk tag).
22
22
  - **Risk level** — `low | medium | high`. The author's assessment of blast radius.
23
23
  - **Rollback plan** — how to revert safely.
24
24
 
25
- ## Routing rule (reuses the gate's escalation)
25
+ ## Routing rule (reuses the gate's count)
26
26
 
27
- | Condition | Required reviewers |
28
- |-----------|--------------------|
29
- | `low` / `medium`, no sensitive surface | **base rule:** owner + 1 reviewer |
30
- | `high`, OR a touched contract/auth/payments surface | base rule **plus one domain-owner approval per touched domain** |
27
+ The count is `base 1 + risk step`:
31
28
 
32
- This is exactly `yad-review-gate`'s rule (`references/gating.md`): the base rule is owner + 1
33
- reviewer; escalation adds a domain-owner per touched domain. The PR template applies the same logic at
34
- the code-review boundary so a risky PR cannot be approved by just any two people. The **approvals are
35
- recorded by the engineer review (Step E) through `yad-review-gate`** — the template and `risk-route.sh`
36
- only *route* (advisory); they never approve or merge.
29
+ | Condition | Risk step | Approvers |
30
+ |-----------|-----------|-----------|
31
+ | `low` / `medium`, no contract surface | +0 | 1 (the base) |
32
+ | `high` | +1 | 2 |
33
+ | contract surface touched | +2 | 3 |
34
+
35
+ `high` risk and a touched contract surface together take the **larger** step (contract, +2), never the
36
+ sum. Approvers are counted as distinct people, and the base approver should not be the author.
37
+
38
+ **The count is reported, not enforced, on a code PR.** Branch protection holds the merge — at least 1
39
+ approval (GitHub blocks self-approval; GitLab only if its settings do). The risk step is **advisory**
40
+ here. The capacity cap (roadmap E72) caps a count at the active people less one. It is reported, not
41
+ enforced, on the Shape gates for now, and on the Build half it stays reported even once the Shape cap
42
+ is enforced: `yad open-pr` prints the count capped by the active people, while `risk-route.sh` cannot cap, because a code repo's CI has no Product to count people
43
+ from. There are no roles: no owner, no reviewer, no
44
+ domain owner. This is `yad-review-gate`'s rule (`references/gating.md`) applied at the code-review
45
+ boundary. The **approvals are recorded by the engineer review (Step E) through `yad-review-gate`** — the
46
+ template and `risk-route.sh` only *route* (advisory); they never approve or merge.
47
+
48
+ **The risk map counts too (E66).** A code repo's `.sdlc/risk-map` (E65) gives each directory a level —
49
+ `high`, `medium` or `low`. A change that touches a `high` directory adds the high step (+1), the same
50
+ step a body saying `Risk level: high` adds. The rules:
51
+
52
+ - The map is read from the **base branch**, never the PR's own copy, so a PR cannot lower its count by
53
+ editing or deleting the map.
54
+ - The **larger** step wins. A body saying `low` on a change to a `high` directory still counts +1, and
55
+ the output says the two disagree. A body saying `high` still counts +1 when the map says `low`.
56
+ - A `guessed` level counts as a `confirmed` one does: a level only raises the count, never lowers it.
57
+ - `medium` is printed and adds nothing. An `unset` line, or a directory no line covers, adds nothing.
58
+ - A deleted or moved file counts where it was.
59
+ - If the map cannot be read (no base, a shallow clone, a newer map version, or a `checks/risk-map-check.sh`
60
+ older than this count, which `yad update` refreshes), the output says so and counts the body alone. It
61
+ never guesses a level.
37
62
 
38
63
  ## risk-route.sh
39
64
 
40
- `bash checks/risk-route.sh <pr-description-file>` parses the Impact & Risk block and prints the required
41
- reviewers. Example (high risk, two domains):
65
+ `bash checks/risk-route.sh <pr-description-file> [<base>]` parses the Impact & Risk block, asks
66
+ `checks/risk-map-check.sh --level` for the level the change takes from the base branch's risk map, and
67
+ prints the approver count. **Run it inside the code repo, on the PR's branch.** On the base branch itself
68
+ the change is empty, and it says so. `<base>` defaults the way the checks resolve it (the configured
69
+ `default_branch`, then `origin/HEAD`, then `origin/main`). Example (high risk, two domains):
42
70
 
43
71
  ```
44
72
  Risk level: high
45
73
  Contract surface touched: no
46
74
  Domains touched: backend, mobile
47
- ROUTE: ESCALATED (risk: high) -> owner + 1 reviewer PLUS one domain-owner approval per touched domain
48
- (same escalation as yad-review-gate). Required domain owners:
49
- - domain-owner: backend
50
- - domain-owner: mobile
75
+ Risk map (origin/main): low — nothing this change touches is high
76
+ ROUTE: 2 approvers = base 1 + high risk 1 (risk: high)
77
+ Branch protection (when configured) holds the merge, not this count: at least 1 approval (GitHub blocks self-approval; GitLab only if its settings do).
78
+ The risk step is advisory, and not capped here — `yad open-pr` shows the count capped by the active people.
79
+ Ask reviewers who know the touched domains:
80
+ - backend
81
+ - mobile
82
+ ```
83
+
84
+ With a touched contract surface the line reads `ROUTE: 3 approvers = base 1 + contract risk 2 (contract
85
+ surface touched)`. With neither it reads `ROUTE: 1 approver = base 1 (no risk step).` When the map raises
86
+ a body that says `low`:
87
+
51
88
  ```
89
+ Risk map (origin/main): high — src/payments/ (guessed)
90
+ worked there in the last 30 days: Alice (@alice)
91
+ ROUTE: 2 approvers = base 1 + high risk 1 (high on the risk map: src/payments/ (guessed))
92
+ The body says Risk level: low, but the risk map on origin/main marks src/payments/ (guessed) high — the larger counts.
93
+ Branch protection (when configured) holds the merge, not this count: at least 1 approval (GitHub blocks self-approval; GitLab only if its settings do).
94
+ The risk step is advisory, and not capped here — `yad open-pr` shows the count capped by the active people.
95
+ This change touches src/payments/ (guessed), so it asks for an approval from someone who has
96
+ committed there in the last 30 days (its own authors left out):
97
+ - Alice (@alice)
98
+ Ask reviewers who know the touched domains:
99
+ (Domains line not filled in — list each touched domain so the right reviewers can be asked.)
100
+ ```
101
+
102
+ **Proven history (E67).** When a `high` directory is touched, the script also names the people who have
103
+ committed there in the **last 30 days**, read from the base branch's history, with this change's own
104
+ authors left out (an approval has to come from someone else). A robot is never listed. A person is shown
105
+ as their git name, plus `(@login)` only when their commit address is a platform `noreply` one — yadflow
106
+ prints no e-mail addresses. A name holding an `@` is shown as the bare login (`- @carol`), else as `a name
107
+ that is an e-mail address` or `a name written like a login`, so a printed `@word` is always a real login. "Recent" is git's `--since`, which uses the **committer** date, so a rebased
108
+ or squashed commit counts from when it landed. Work in a `low` directory *inside* a `high` one is not
109
+ history for the `high` one: the deepest map line decides, and git applies that itself — one query per
110
+ directory, excluding the listed directories below it, so no file name is ever read back out of git.
111
+
112
+ When a change touches more than one `high` directory, the people are printed as ONE list: someone who
113
+ has worked in any of them meets the ask, and the output does not say who worked where.
114
+
115
+ Two answers are not a list of people, and both are said plainly:
116
+
117
+ - `nobody else has committed there in the last 30 days` — the count stands on its own.
118
+ - `who has worked there lately: not read — …` — a shallow clone, or git could not read the history. The
119
+ ask is unknown, never "nobody".
120
+
121
+ The CI `risk-map` job prints the map's part on every PR as a `COUNT [risk-map]:` line. It does not read
122
+ the body, so `risk-route.sh` is where the two meet. `yad open-pr` prints the same joined count once the
123
+ PR is open, and leaves the body's level as the author wrote it.
52
124
 
53
- It is **advisory** — not a blocking gate. CI may run it to comment the required reviewers; the human
54
- review (Step E) still owns the merge.
125
+ It is **advisory** — not a blocking gate, and not a CI job (the user's decision, 2026-09-18): the CI
126
+ `risk-map` job prints the map's half, and the human review (Step E), which runs this script, owns the
127
+ merge. No reviewers are requested automatically: ask them on the PR/MR itself.
@@ -7,13 +7,15 @@
7
7
  # requires `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled `Risk level:` (low|medium|high).
8
8
  # --profile hub — the Shape artifact-review template (templates/hub/<platform>/):
9
9
  # requires `## Artifact under review`, `## Impact & Risk (front-half)` (or `(Shape)`), `## Checklist`, and a `Risk tags:` line.
10
- # BUT only for review/EP-* head branches. Every other hub PR is a tooling/code change to the hub
10
+ # BUT only for review/EP-* head branches. Every other Product PR is a tooling/code change to the Product
11
11
  # itself and uses the code task template instead; pass the head ref via --head so the gate knows
12
- # which template to require. With no --head, the hub profile stays strict (artifact-review template).
12
+ # which template to require. With no --head, the Product profile stays strict (artifact-review template).
13
13
  # Branch name is not enough on its own: a non-review head that actually changes Shape
14
14
  # artifacts (epics/**) would otherwise slip past the review workflow with only the code template.
15
15
  # Pass the PR's changed paths via --changed <file> (one path per line); when they touch epics/**
16
16
  # on a non-review head the gate FAILS — artifact changes must go through a review/EP-* PR.
17
+ # Build the list with `git -c core.quotePath=false diff --no-renames --name-only`: without
18
+ # --no-renames a rename lists only its new path, and the removed artifact is never seen.
17
19
  # The body is passed as a FILE path (single positional arg); CI writes the event body to a temp file
18
20
  # (GitHub: github.event.pull_request.body; GitLab: $CI_MERGE_REQUEST_DESCRIPTION).
19
21
  set -euo pipefail
@@ -33,11 +35,32 @@ while [ $# -gt 0 ]; do
33
35
  *) ARGS+=("$1"); shift ;;
34
36
  esac
35
37
  done
36
- case "$PROFILE" in code|hub) ;; *) echo "FAIL [pr-template]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
38
+ case "$PROFILE" in code|hub|product) ;; *) echo "FAIL [pr-template]: unknown --profile '$PROFILE' (code|hub|product)."; exit 1 ;; esac
39
+ # `product` is the new name for the `hub` profile and BOTH are accepted.
40
+ #
41
+ # Not because the two sides update separately — they do not: this script and the workflow that passes
42
+ # the flag are both in PRODUCT_WIRING (cli/manifest.mjs) and land together on one `yad update`. It is
43
+ # the plain add-before-remove ladder instead: accept the new spelling now, switch the workflow
44
+ # templates to emit it in a later release, drop the old one after that. Every shipped template still
45
+ # passes `--profile hub` today, so this arm is dead weight until that switch — which is the point.
46
+ #
47
+ # Normalised to `hub` immediately, so nothing below has to know there are two spellings. That is
48
+ # load-bearing in pr-title.sh and pr-template.sh: leave `$PROFILE` as `product` and the `= hub`
49
+ # branch is skipped, taking the whole review-branch arm with it — including the guard that stops a
50
+ # plain code title carrying an artifact change past its review.
51
+ [ "$PROFILE" = product ] && PROFILE=hub
37
52
 
38
- # True when the PR changes a Shape artifact (anything under epics/**). Reads the --changed list
53
+
54
+ # True when the PR changes a Shape artifact (anything under epics/** — or foundation/**, the Product level, E75). Reads the --changed list
39
55
  # of paths CI computed from the PR diff; with no list (direct caller / test) it reports false.
40
- artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && grep -qE '^epics/' "$CHANGED"; }
56
+ # A step owner file (`yad assign`, E47) is not an artifact: an assignment is advice, reviewed by nobody, and
57
+ # a review/EP-* PR would advance the step on merge. So a PR of owner files alone is not an artifact change.
58
+ # No `-q` on the second grep: under pipefail an early exit would SIGPIPE the first and read as "no artifact".
59
+ # `"?`: git still C-quotes a path holding `"`, `\`, a tab or a newline, even with core.quotePath=false. Such a
60
+ # line counts as an artifact, and the owner exemption never matches it — so it fails closed.
61
+ # `LC_ALL=C grep -a` on both: with quotePath off a path's high bytes arrive raw, and GNU grep under a UTF-8
62
+ # locale drops a line that is not valid UTF-8 from its output — the pipe would then read as "no artifact".
63
+ artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && LC_ALL=C grep -aE '^"?(epics|foundation)/' "$CHANGED" | LC_ALL=C grep -avE '^(epics/EP-[^/]+|foundation)/\.sdlc/owners/[^/]+\.json$' >/dev/null; }
41
64
 
42
65
  BODY="${ARGS[0]:-}"
43
66
  if [ -z "$BODY" ] || [ ! -f "$BODY" ]; then
@@ -75,7 +98,7 @@ require_heading() {
75
98
  }
76
99
 
77
100
  # The code task template: `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled `Risk level:`.
78
- # Used by the code profile and by hub tooling PRs (any head branch that is not review/EP-*).
101
+ # Used by the code profile and by Product tooling PRs (any head branch that is not review/EP-*).
79
102
  check_code_body() {
80
103
  require_heading '## Summary' '## Summary'
81
104
  require_heading '## Impact & Risk' '## Impact & Risk'
@@ -95,11 +118,11 @@ check_code_body() {
95
118
  check_hub_body() {
96
119
  require_heading '## Artifact under review' '## Artifact under review'
97
120
  # Add before you remove (change-safety rule 3). `front-half` is the ORIGINAL spelling and it is
98
- # still what `yad gate open` emits and what the shipped hub template carries; `Shape` is the
121
+ # still what `yad gate open` emits and what the shipped Product template carries; `Shape` is the
99
122
  # replacement, accepted here first so that this script is lenient BEFORE anything starts writing
100
123
  # the new wording. That ordering is the whole point: this file is refreshed by `yad update`
101
- # (HUB_WIRING, cli/manifest.mjs), but the PR template beside it is NOT — it is installed only by
102
- # the `yad-pr-template repo:hub action: wire` skill, which nothing runs automatically. So a hub
124
+ # (PRODUCT_WIRING, cli/manifest.mjs), but the PR template beside it is NOT — it is installed only by
125
+ # the `yad-pr-template repo:hub action: wire` skill, which nothing runs automatically. So a Product
103
126
  # WILL sit with a refreshed checker next to an old template, and the checker has to accept both.
104
127
  # The emitter flips to `(Shape)` in the next major; only after that may `front-half` be dropped.
105
128
  require_heading '## Impact & Risk \((front-half|Shape)\)' '## Impact & Risk (front-half)'
@@ -115,11 +138,11 @@ if [ "$PROFILE" = hub ]; then
115
138
  case "$HEADREF" in
116
139
  review/EP-*|"") check_hub_body ;; # artifact-review PR (or unknown head — stay strict)
117
140
  *)
118
- # tooling/code change to the hub itself — UNLESS it changes Shape artifacts (epics/**),
141
+ # tooling/code change to the Product itself — UNLESS it changes Shape artifacts (epics/**),
119
142
  # which must go through a review/EP-* PR. Without this guard a non-review head could carry an
120
143
  # artifact change past the Shape review with only the code template.
121
144
  if artifact_changed; then
122
- echo "FAIL [pr-template]: head '${HEADREF}' changes Shape artifacts (epics/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
145
+ echo "FAIL [pr-template]: head '${HEADREF}' changes Shape artifacts (epics/** or foundation/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
123
146
  rc=1
124
147
  else
125
148
  check_code_body; KIND="hub-tooling"
@@ -6,13 +6,15 @@
6
6
  # the squash-merge subject). Keep <type> in sync with cli/manifest.mjs COMMIT_TYPES.
7
7
  # --profile hub — a Shape artifact-review title "review: <artifact> (EP-<slug>)", the shape
8
8
  # `yad gate open` creates (cli/gate.mjs) — BUT only for review/EP-* head branches. Every other
9
- # hub PR is a tooling/code change to the hub itself and follows the code convention; pass the
9
+ # Product PR is a tooling/code change to the Product itself and follows the code convention; pass the
10
10
  # head ref via --head so the gate can tell the two apart (a tooling PR has no EP artifact to
11
- # review). With no --head, the hub profile stays strict (requires the review shape).
11
+ # review). With no --head, the Product profile stays strict (requires the review shape).
12
12
  # Branch name is not enough on its own: a non-review head that actually changes Shape
13
13
  # artifacts (epics/**) would otherwise slip past the review workflow with a plain code title.
14
14
  # Pass the PR's changed paths via --changed <file> (one path per line); when they touch epics/**
15
15
  # on a non-review head the gate FAILS — artifact changes must go through a review/EP-* PR.
16
+ # Build the list with `git -c core.quotePath=false diff --no-renames --name-only`: without
17
+ # --no-renames a rename lists only its new path, and the removed artifact is never seen.
16
18
  # The title is passed as the (single) positional arg; CI injects it from the event payload
17
19
  # (GitHub: github.event.pull_request.title; GitLab: $CI_MERGE_REQUEST_TITLE).
18
20
  set -euo pipefail
@@ -32,11 +34,32 @@ while [ $# -gt 0 ]; do
32
34
  *) ARGS+=("$1"); shift ;;
33
35
  esac
34
36
  done
35
- case "$PROFILE" in code|hub) ;; *) echo "FAIL [pr-title]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
37
+ case "$PROFILE" in code|hub|product) ;; *) echo "FAIL [pr-title]: unknown --profile '$PROFILE' (code|hub|product)."; exit 1 ;; esac
38
+ # `product` is the new name for the `hub` profile and BOTH are accepted.
39
+ #
40
+ # Not because the two sides update separately — they do not: this script and the workflow that passes
41
+ # the flag are both in PRODUCT_WIRING (cli/manifest.mjs) and land together on one `yad update`. It is
42
+ # the plain add-before-remove ladder instead: accept the new spelling now, switch the workflow
43
+ # templates to emit it in a later release, drop the old one after that. Every shipped template still
44
+ # passes `--profile hub` today, so this arm is dead weight until that switch — which is the point.
45
+ #
46
+ # Normalised to `hub` immediately, so nothing below has to know there are two spellings. That is
47
+ # load-bearing in pr-title.sh and pr-template.sh: leave `$PROFILE` as `product` and the `= hub`
48
+ # branch is skipped, taking the whole review-branch arm with it — including the guard that stops a
49
+ # plain code title carrying an artifact change past its review.
50
+ [ "$PROFILE" = product ] && PROFILE=hub
36
51
 
37
- # True when the PR changes a Shape artifact (anything under epics/**). Reads the --changed list
52
+
53
+ # True when the PR changes a Shape artifact (anything under epics/** — or foundation/**, the Product level, E75). Reads the --changed list
38
54
  # of paths CI computed from the PR diff; with no list (direct caller / test) it reports false.
39
- artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && grep -qE '^epics/' "$CHANGED"; }
55
+ # A step owner file (`yad assign`, E47) is not an artifact: an assignment is advice, reviewed by nobody, and
56
+ # a review/EP-* PR would advance the step on merge. So a PR of owner files alone is not an artifact change.
57
+ # No `-q` on the second grep: under pipefail an early exit would SIGPIPE the first and read as "no artifact".
58
+ # `"?`: git still C-quotes a path holding `"`, `\`, a tab or a newline, even with core.quotePath=false. Such a
59
+ # line counts as an artifact, and the owner exemption never matches it — so it fails closed.
60
+ # `LC_ALL=C grep -a` on both: with quotePath off a path's high bytes arrive raw, and GNU grep under a UTF-8
61
+ # locale drops a line that is not valid UTF-8 from its output — the pipe would then read as "no artifact".
62
+ artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && LC_ALL=C grep -aE '^"?(epics|foundation)/' "$CHANGED" | LC_ALL=C grep -avE '^(epics/EP-[^/]+|foundation)/\.sdlc/owners/[^/]+\.json$' >/dev/null; }
40
63
 
41
64
  TITLE="${ARGS[0]:-}"
42
65
  if [ -z "$TITLE" ]; then
@@ -47,7 +70,7 @@ fi
47
70
  TYPES='feat|fix|docs|refactor|test|perf|build|ci|chore|revert'
48
71
 
49
72
  # Conventional-Commits subject (optional scope + breaking `!`), no trailing period. Used by the code
50
- # profile and by hub tooling PRs (any head branch that is not review/EP-*).
73
+ # profile and by Product tooling PRs (any head branch that is not review/EP-*).
51
74
  check_code_title() {
52
75
  if ! printf '%s' "$TITLE" | grep -qE "^(${TYPES})(\([a-z0-9._-]+\))?!?: .+"; then
53
76
  echo "FAIL [pr-title]: '${TITLE}' is not '<type>(<scope>)?!?: <description>' (type one of: ${TYPES//|/, })."
@@ -69,15 +92,15 @@ if [ "$PROFILE" = hub ]; then
69
92
  echo "PASS [pr-title]: '${TITLE}' (profile: hub, artifact-review)"
70
93
  exit 0
71
94
  fi
72
- echo "FAIL [pr-title]: '${TITLE}' is not a hub review title 'review: <artifact> (EP-<slug>)'."
95
+ echo "FAIL [pr-title]: '${TITLE}' is not a Product review title 'review: <artifact> (EP-<slug>)'."
73
96
  exit 1
74
97
  ;;
75
98
  *)
76
- # Any other hub PR is a tooling/code change to the hub itself — UNLESS it changes Shape
99
+ # Any other Product PR is a tooling/code change to the Product itself — UNLESS it changes Shape
77
100
  # artifacts (epics/**), which must go through a review/EP-* PR. Without this guard a non-review
78
101
  # head could carry an artifact change past the Shape review with only a code title.
79
102
  if artifact_changed; then
80
- echo "FAIL [pr-title]: head '${HEADREF}' changes Shape artifacts (epics/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
103
+ echo "FAIL [pr-title]: head '${HEADREF}' changes Shape artifacts (epics/** or foundation/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
81
104
  exit 1
82
105
  fi
83
106
  # tooling only — fall through to the code convention.
@@ -1,11 +1,24 @@
1
1
  #!/usr/bin/env bash
2
- # Risk routing (Phase 3 build plan §D). Reads a PR/MR description's Impact & Risk block and prints the
3
- # required reviewers, reusing yad-review-gate's escalation: a `high` risk level — or a touched
4
- # contract/auth/payments surface — requires a domain-owner approval per touched domain, on top of the
5
- # base rule (owner + 1 reviewer). Advisory: it ROUTES the human review; it does not approve or merge.
2
+ # Risk routing (Phase 3 build plan §D). Reads a PR/MR description's Impact & Risk block and prints how
3
+ # many approvers the change asks for, as the same sum the review gate prints: base 1, plus a risk step —
4
+ # `high` risk +1, a touched contract surface +2, the larger of the two and never their sum. The count is
5
+ # advisory on the Build half: branch protection holds the merge, and this script cannot apply the capacity
6
+ # cap (E72), which needs the Product's count of active people — `yad open-pr` shows the capped count.
7
+ # There are no roles and no named owners: yadflow keeps no list of people (E62), so when a risk step
8
+ # raises the count the touched domains are printed as a hint for whom to ask (with no risk step it prints
9
+ # the base line alone). Advisory: it ROUTES the human review; it does not approve or merge.
10
+ #
11
+ # E66 — the risk map counts too. Run inside the code repo, ON THE PR'S BRANCH, it also asks
12
+ # `checks/risk-map-check.sh --level` which directories the change touches on the BASE branch's
13
+ # `.sdlc/risk-map`: a `high` one adds the high step (+1). The largest step wins, so the body can raise
14
+ # the count and never lower it — a body saying `low` on a change to a `high` directory still counts +1,
15
+ # and the output says the two disagree. When the map cannot be read, it says so and counts the body
16
+ # alone; it never guesses a level.
17
+ #
18
+ # risk-route.sh <pr-description-file> [<base>] <base> defaults the way the checks resolve it
6
19
  set -euo pipefail
7
20
 
8
- BODY="${1:?usage: risk-route.sh <pr-description-file>}"
21
+ BODY="${1:?usage: risk-route.sh <pr-description-file> [<base>]}"
9
22
  [ -f "$BODY" ] || { echo "risk-route: file not found: $BODY" >&2; exit 2; }
10
23
 
11
24
  # Value side of the FIRST line matching a label regex, with any HTML comment + markdown markers
@@ -24,21 +37,101 @@ echo "Risk level: ${risk:-unspecified}"
24
37
  echo "Contract surface touched: ${contract:-unspecified}"
25
38
  echo "Domains touched: ${domains:-unspecified}"
26
39
 
27
- if [ "$risk" = "high" ] || [ "$contract" = "yes" ]; then
28
- why=""
29
- [ "$risk" = "high" ] && why="risk: high"
30
- [ "$contract" = "yes" ] && why="${why:+$why, }contract surface touched"
31
- echo "ROUTE: ESCALATED (${why}) -> owner + 1 reviewer PLUS one domain-owner approval per touched domain"
32
- echo " (same escalation as yad-review-gate). Required domain owners:"
40
+ # The map's level for this change, from the risk-map check beside this script (wired together as
41
+ # checks/), or from the repo's checks/ when this copy runs from elsewhere. Machine lines, see that file.
42
+ here="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" || here=""
43
+ checker=""
44
+ for c in "${here:+$here/}risk-map-check.sh" checks/risk-map-check.sh; do
45
+ if [ -f "$c" ]; then checker="$c"; break; fi
46
+ done
47
+ if [ -z "$checker" ]; then
48
+ lv="UNKNOWN checks/risk-map-check.sh is not here — wire it with \`yad update\`"
49
+ else
50
+ lv="$(bash "$checker" --level ${2:+"$2"} 2>/dev/null)" || lv="UNKNOWN checks/risk-map-check.sh failed"
51
+ fi
52
+ line_of() { printf '%s\n' "$lv" | sed -n "s/^$1 //p" | head -1; }
53
+ dirs_of() { printf '%s\n' "$lv" | awk -v want="$1" '$1 == "DIR" && $3 == want { printf "%s%s%s", sep, $2, ($4 == "guessed") ? " (guessed)" : ""; sep = ", " }'; }
54
+ # A risk-map check from before E66 has no --level mode: it reads the flag as a base and prints warnings,
55
+ # none of the lines above. Reading that as "nothing is high" would be a guess, so it is unknown instead.
56
+ if [ -z "$(line_of UNKNOWN)$(line_of NOMAP)$(line_of LEVEL)" ]; then
57
+ lv="UNKNOWN checks/risk-map-check.sh cannot count the risk map yet (it predates E66) — update it with \`yad update\`"
58
+ fi
59
+ map_base="$(line_of BASE)"
60
+ map_high=""
61
+ # E67 — the people the check found with recent commits in those `high` directories: `WHO <login|-> <name>`.
62
+ # `$1 = ""` would rebuild the line with one space between fields and squash a name's own spacing.
63
+ who_list() { printf '%s\n' "$lv" | sed -n 's/^WHO //p' | awk '{ login = $1; name = $0; sub(/^[^ ]* /, "", name); printf "%s%s%s", sep, name, (login == "-" || name == "@" login) ? "" : " (@" login ")"; sep = ", " }'; }
64
+ if [ -n "$(line_of UNKNOWN)" ]; then
65
+ echo "Risk map: not counted — $(line_of UNKNOWN). Only the body is counted."
66
+ elif [ -n "$(line_of NOMAP)" ]; then
67
+ echo "Risk map: none — $(line_of NOMAP), so no directory adds a step."
68
+ else
69
+ map_high="$(dirs_of high)"
70
+ map_medium="$(dirs_of medium)"
71
+ if [ "$(line_of FILES)" = "0" ]; then
72
+ echo "Risk map (${map_base}): this checkout changes no file against ${map_base} — run it on the PR's branch."
73
+ elif [ -n "$map_high" ]; then
74
+ echo "Risk map (${map_base}): high — ${map_high}"
75
+ else
76
+ echo "Risk map (${map_base}): $(line_of LEVEL) — nothing this change touches is high"
77
+ fi
78
+ [ -z "$map_medium" ] || echo " medium (reported only, adds nothing): ${map_medium}"
79
+ if [ -n "$map_high" ]; then
80
+ hist_unknown="$(line_of HISTUNKNOWN)"; who="$(who_list)"
81
+ # A check older than E67 answers nothing about history: no WHO, and no HISTNONE saying it looked.
82
+ # Reading that silence as "nobody" would drop the ask — the same mistake as reading a failure as zero.
83
+ if [ -z "$hist_unknown" ] && [ -z "$who" ] && [ -z "$(printf '%s\n' "$lv" | sed -n 's/^HISTNONE$/x/p')" ]; then
84
+ hist_unknown="checks/risk-map-check.sh cannot answer this yet (it predates E67) — update it with \`yad update\`"
85
+ fi
86
+ if [ -n "$hist_unknown" ]; then
87
+ echo " who has worked there lately: not read — ${hist_unknown}"
88
+ elif [ -n "$who" ]; then
89
+ echo " worked there in the last 30 days: ${who}"
90
+ else
91
+ echo " nobody else has committed there in the last 30 days"
92
+ fi
93
+ fi
94
+ fi
95
+
96
+ step=0
97
+ tier=""
98
+ why=""
99
+ if [ "$risk" = "high" ]; then step=1; tier=high; why="risk: high"; fi
100
+ if [ -n "$map_high" ]; then
101
+ [ "$step" -ge 1 ] || { step=1; tier=high; }
102
+ why="${why:+$why, }high on the risk map: ${map_high}"
103
+ fi
104
+ if [ "$contract" = "yes" ]; then step=2; tier=contract; why="${why:+$why, }contract surface touched"; fi
105
+
106
+ if [ "$step" -gt 0 ]; then
107
+ echo "ROUTE: $((1 + step)) approvers = base 1 + ${tier} risk ${step} (${why})"
108
+ # The body can raise the count and never lower it: say so when it tried to.
109
+ if [ -n "$map_high" ] && [ "$risk" != "high" ]; then
110
+ echo " The body says Risk level: ${risk:-unspecified}, but the risk map on ${map_base} marks ${map_high} high — the larger counts."
111
+ fi
112
+ echo " Branch protection (when configured) holds the merge, not this count: at least 1 approval (GitHub blocks self-approval; GitLab only if its settings do)."
113
+ echo " The risk step is advisory, and not capped here — \`yad open-pr\` shows the count capped by the active people."
114
+ # E67 — a change to a `high` directory asks for an approval from someone who has worked there in the
115
+ # last 30 days. Reported, like the count: it names who can meet it and blocks nothing. With nobody to
116
+ # name (or a history that could not be read), the touched domains stay the only hint.
117
+ if [ -n "$map_high" ] && [ -n "$(who_list)" ]; then
118
+ echo " This change touches ${map_high}, so it asks for an approval from someone who has"
119
+ echo " committed there in the last 30 days (its own authors left out):"
120
+ printf '%s\n' "$lv" | sed -n 's/^WHO //p' | while IFS= read -r row; do
121
+ login="${row%% *}"; name="${row#* }"
122
+ if [ "$login" = "-" ] || [ "$name" = "@${login}" ]; then echo " - ${name}"; else echo " - ${name} (@${login})"; fi
123
+ done
124
+ fi
125
+ echo " Ask reviewers who know the touched domains:"
33
126
  case "$domains" in
34
127
  ""|*"<"*|*"…"*|*"|"*)
35
- echo " (Domains line not filled in — list each touched domain to route the owners.)" ;;
128
+ echo " (Domains line not filled in — list each touched domain so the right reviewers can be asked.)" ;;
36
129
  *)
37
130
  printf '%s\n' "$domains" | tr ',' '\n' | while IFS= read -r d; do
38
131
  d="$(printf '%s' "$d" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//')"
39
- [ -n "$d" ] && echo " - domain-owner: $d"
132
+ if [ -n "$d" ]; then echo " - $d"; fi # not `&&`: an empty last item (`a, b,`) would exit 1
40
133
  done ;;
41
134
  esac
42
135
  else
43
- echo "ROUTE: base rule -> owner + 1 reviewer (no domain-owner escalation)."
136
+ echo "ROUTE: 1 approver = base 1 (no risk step)."
44
137
  fi
@@ -12,12 +12,14 @@
12
12
  <!-- Fill every field. risk-route.sh + the engineer review read this block. -->
13
13
  - **Domains / repos touched:** <backend | mobile | …>
14
14
  - **Contract surface touched:** no <!-- yes => needs Contract-Change + a re-locked contract (contract-check) -->
15
- - **Risk level:** low <!-- low | medium | high — high (or a contract/auth/payments surface) routes to domain owners -->
15
+ - **Risk level:** low <!-- low | medium | high — high (or a touched contract surface) raises the advisory approver count -->
16
16
  - **Rollback plan:** <how to revert if this misbehaves>
17
17
 
18
- > **Routing:** `low`/`medium` → base rule (owner + 1 reviewer). `high` (or a touched
19
- > contract/auth/payments surface) → **plus one domain-owner approval per touched domain**, the same
20
- > escalation `yad-review-gate` applies. Run `bash checks/risk-route.sh <this-description>` to list them.
18
+ > **Routing:** the merge needs **1 approval, which should not be the author’s own**. `high` risk adds 1
19
+ > and a touched contract surface adds 2 to the count (the larger, never the sum); that step is advisory
20
+ > (`yad open-pr` shows it capped by the active people). A change to a directory the base branch's `.sdlc/risk-map` marks `high` adds 1
21
+ > too, even when this says `low`. Run `bash checks/risk-route.sh <this-description>` on this branch to
22
+ > print the count.
21
23
 
22
24
  ## Testing
23
25
  <!-- How the acceptance criteria were exercised. Tests must exercise behavior, not just pass. -->
@@ -27,4 +29,4 @@
27
29
  - [ ] No contract-surface change without `Contract-Change: yes` + a re-locked contract (contract-check gate)
28
30
  - [ ] Lint, build, and tests pass (build/test/lint gate)
29
31
  - [ ] Diff stays inside the files the task's spec declared (≤3 where possible)
30
- - [ ] Impact & Risk filled; `high` risk adds the required domain-owner reviewers
32
+ - [ ] Impact & Risk filled; for `high` risk, ask reviewers who know the touched domains
@@ -16,12 +16,14 @@
16
16
  <!-- Fill every field. risk-route.sh + the engineer review read this block. -->
17
17
  - **Domains / repos touched:** <backend | mobile | …>
18
18
  - **Contract surface touched:** no <!-- yes => needs Contract-Change + a re-locked contract (contract-check) -->
19
- - **Risk level:** low <!-- low | medium | high — high (or a contract/auth/payments surface) routes to domain owners -->
19
+ - **Risk level:** low <!-- low | medium | high — high (or a touched contract surface) raises the advisory approver count -->
20
20
  - **Rollback plan:** <how to revert if this misbehaves>
21
21
 
22
- > **Routing:** `low`/`medium` → base rule (owner + 1 reviewer). `high` (or a touched
23
- > contract/auth/payments surface) → **plus one domain-owner approval per touched domain**, the same
24
- > escalation `yad-review-gate` applies. Run `bash checks/risk-route.sh <this-description>` to list them.
22
+ > **Routing:** the merge needs **1 approval, which should not be the author’s own**. `high` risk adds 1
23
+ > and a touched contract surface adds 2 to the count (the larger, never the sum); that step is advisory
24
+ > (`yad open-pr` shows it capped by the active people). A change to a directory the base branch's `.sdlc/risk-map` marks `high` adds 1
25
+ > too, even when this says `low`. Run `bash checks/risk-route.sh <this-description>` on this branch to
26
+ > print the count.
25
27
 
26
28
  ## Testing
27
29
  <!-- How the acceptance criteria were exercised. Tests must exercise behavior, not just pass. -->
@@ -31,6 +33,6 @@
31
33
  - [ ] No contract-surface change without `Contract-Change: yes` + a re-locked contract (contract-check gate)
32
34
  - [ ] Lint, build, and tests pass (build/test/lint gate)
33
35
  - [ ] Diff stays inside the files the task's spec declared (≤3 where possible)
34
- - [ ] Impact & Risk filled; `high` risk adds the required domain-owner reviewers
36
+ - [ ] Impact & Risk filled; for `high` risk, ask reviewers who know the touched domains
35
37
 
36
38
  /assign me
@@ -1,7 +1,7 @@
1
1
  <!-- SDLC HUB PR template — Shape artifact review (epic / architecture+contract / ui-design / stories). -->
2
- <!-- This PR is a REVIEW VEHICLE on the product hub, not a code merge. The file gate (yad-review-gate)
3
- advances the step; do NOT rely on merging this PR to advance. Reviewers approve/comment here, then a
4
- `yad-review-gate action: sync` pulls that into the file ledger. -->
2
+ <!-- This PR is a REVIEW VEHICLE on the Product, not a code merge. The file gate (yad-review-gate)
3
+ advances the step when this PR is MERGED with the approvals met and every thread resolved. Reviewers
4
+ approve/comment here; `yad gate sync` (or CI at merge, with a verified ledger) pulls that into the file ledger. -->
5
5
 
6
6
  ## Artifact under review
7
7
  - Epic: `EP-<slug>`
@@ -14,23 +14,24 @@
14
14
 
15
15
  ## Impact & Risk (front-half)
16
16
  - **Domains / repos touched:** <epic.repos, e.g. backend, mobile>
17
- - **Risk tags:** <none | contract | auth | payments> <!-- contract/auth/payments => escalates to domain owners -->
17
+ - **Risk tags:** <none | contract | auth | payments> <!-- contract/auth/payments => raises the advisory approver count -->
18
18
  - **Contract surface:** <n/a | locked @ sha256:…> <!-- architecture only; a re-lock invalidates prior approvals -->
19
19
 
20
20
  ## Required approvals (yad-review-gate rule)
21
- - Base: **owner + 1 reviewer**.
22
- - Escalated (risk tag set, or a stories PR): **plus one domain-owner per touched repo** — see the
23
- requested reviewers / `domain:<repo>` labels on this PR. Run `bash checks/hub-route.sh <this-description>`
24
- to list them.
21
+ - Full count, in **distinct people**: base 1 + the step's risk step — 3 on a `contract` step, 2 on
22
+ `auth`/`payments`, 1 otherwise (the largest step, never the sum). No approval should be the author’s own.
23
+ - Enforced: **1 approval** (the base). The risk step is **advisory**. `yad gate status <epic>`
24
+ prints the count capped at the active people less one (one seat is left for the author), and
25
+ `yad-hub-bridge`'s `hub-route.sh` run on this description prints the full count.
26
+ - No reviewers are requested automatically — ask them on this PR. The `domain:<repo>` labels show the touched repos.
25
27
 
26
28
  ## How to review (this drives the gate)
27
- - **Approve** this PR to record an `owner` / `reviewer` / `domain-owner` approval in the file ledger
28
- (your platform login maps to your SDLC name + role via `.sdlc/hub.json`'s roster).
29
+ - **Approve** this PR to record your approval in the file ledger, under your platform login (no role).
29
30
  - **Comment / request changes** to record review comments (synced into `reviews/<artifact>--<date>--comments.md`).
30
- - **Do NOT merge to advance** — `yad-review-gate action: sync` + `action: advance` move the step.
31
+ - **Merge to advance** — once the approvals are met and every thread is resolved, merging this PR moves the step (CI records it with a verified ledger; otherwise `yad gate sync`).
31
32
 
32
33
  ## Checklist
33
34
  - [ ] `owner` set in the artifact frontmatter (inherited from `epic.md`)
34
- - [ ] Contract re-locked (`.sdlc/contract-lock.json`) if the surface changed (architecture only)
35
- - [ ] Risk tags reflect the real surface touched (contract/auth/payments escalate)
35
+ - [ ] Contract re-locked (`.sdlc/contract-lock.json`) if the surface changed (architecture only — a short-lane epic has no architecture step and no lock, so tick this only if your route has one)
36
+ - [ ] Risk tags reflect the real surface touched (contract/auth/payments raise the approval count)
36
37
  - [ ] No secrets or tokens in the artifact or this description