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.
- package/CHANGELOG.md +355 -0
- package/README.md +79 -26
- package/bin/commands.mjs +41 -0
- package/bin/yad.mjs +437 -124
- package/cli/artifact-status.mjs +34 -15
- package/cli/checkpoint.mjs +69 -49
- package/cli/codeowners-command.mjs +170 -0
- package/cli/codeowners.mjs +397 -0
- package/cli/commit.mjs +13 -9
- package/cli/companion.mjs +2 -2
- package/cli/dial.mjs +183 -0
- package/cli/docs.mjs +88 -32
- package/cli/doctor.mjs +1472 -97
- package/cli/epic-state.mjs +3478 -232
- package/cli/epic.mjs +506 -0
- package/cli/errors.mjs +4 -1
- package/cli/gate.mjs +1002 -209
- package/cli/history.mjs +556 -0
- package/cli/hook.mjs +266 -55
- package/cli/hubcommit.mjs +6 -17
- package/cli/index-command.mjs +87 -0
- package/cli/ledger.mjs +57 -7
- package/cli/lib.mjs +184 -18
- package/cli/manifest.mjs +367 -56
- package/cli/migrate.mjs +726 -53
- package/cli/mode.mjs +170 -0
- package/cli/next.mjs +349 -90
- package/cli/openpr.mjs +191 -39
- package/cli/people.mjs +654 -0
- package/cli/plan.mjs +417 -132
- package/cli/platform.mjs +110 -129
- package/cli/product-index.mjs +287 -0
- package/cli/protection.mjs +706 -0
- package/cli/reconcile.mjs +38 -12
- package/cli/repo-publish.mjs +24 -26
- package/cli/repo.mjs +23 -14
- package/cli/report.mjs +21 -15
- package/cli/review.mjs +24 -27
- package/cli/riskmap-command.mjs +289 -0
- package/cli/riskmap.mjs +373 -0
- package/cli/setup.mjs +139 -287
- package/cli/ship.mjs +7 -6
- package/cli/skill.mjs +180 -0
- package/cli/skip.mjs +211 -30
- package/cli/thread.mjs +42 -17
- package/cli/tidy.mjs +20 -20
- package/cli/update-commit.mjs +22 -22
- package/cli/usage.mjs +115 -109
- package/package.json +3 -3
- package/skills/sdlc/config.yaml +166 -87
- package/skills/sdlc/module-help.csv +35 -35
- package/skills/yad-analysis/SKILL.md +125 -65
- package/skills/yad-architecture/SKILL.md +34 -23
- package/skills/yad-architecture/references/contract-format.md +10 -8
- package/skills/yad-backfill/SKILL.md +14 -8
- package/skills/yad-backfill/references/backfill.md +1 -1
- package/skills/yad-change/SKILL.md +127 -52
- package/skills/yad-change/references/triage.md +42 -28
- package/skills/yad-checks/SKILL.md +89 -45
- package/skills/yad-checks/references/check-gates.md +315 -92
- package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
- package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
- package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
- package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
- package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
- package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
- package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
- package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
- package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
- package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
- package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
- package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
- package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
- package/skills/yad-commit/SKILL.md +6 -6
- package/skills/yad-connect-design/SKILL.md +6 -6
- package/skills/yad-connect-design/references/design-context.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +2 -2
- package/skills/yad-connect-docs/SKILL.md +12 -12
- package/skills/yad-connect-docs/references/docs-registry.md +1 -1
- package/skills/yad-connect-learning/SKILL.md +5 -5
- package/skills/yad-connect-learning/references/learning-registry.md +2 -2
- package/skills/yad-connect-repos/SKILL.md +92 -54
- package/skills/yad-connect-repos/references/code-context.md +6 -6
- package/skills/yad-connect-repos/references/hub-config.md +68 -58
- package/skills/yad-connect-repos/references/repos-registry.md +10 -9
- package/skills/yad-connect-repos/references/risk-map.md +81 -0
- package/skills/yad-connect-testing/SKILL.md +6 -6
- package/skills/yad-connect-testing/references/testing-context.md +3 -4
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-defects/SKILL.md +8 -8
- package/skills/yad-discovery/SKILL.md +130 -94
- package/skills/yad-discovery/references/discovery-schema.md +23 -7
- package/skills/yad-discovery/references/foundation-schema.md +374 -0
- package/skills/yad-docs/SKILL.md +16 -11
- package/skills/yad-docs/references/data-mapping.md +9 -7
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-docs-overview/SKILL.md +32 -17
- package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
- package/skills/yad-docs-sync/SKILL.md +10 -5
- package/skills/yad-docs-sync/references/staleness.md +8 -7
- package/skills/yad-engineer-review/SKILL.md +88 -24
- package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
- package/skills/yad-epic/SKILL.md +178 -100
- package/skills/yad-epic/references/state-schema.md +626 -117
- package/skills/yad-hub-bridge/SKILL.md +66 -48
- package/skills/yad-hub-bridge/references/bridge.md +110 -83
- package/skills/yad-hub-bridge/references/login-roster.md +163 -70
- package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
- package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
- package/skills/yad-implement/SKILL.md +29 -15
- package/skills/yad-implement/references/implement-conventions.md +2 -2
- package/skills/yad-learn/SKILL.md +9 -9
- package/skills/yad-learn/references/learning-state.md +2 -2
- package/skills/yad-open-pr/SKILL.md +64 -29
- package/skills/yad-pair-review/SKILL.md +18 -16
- package/skills/yad-pair-review/references/session-state.md +4 -4
- package/skills/yad-pr-template/SKILL.md +48 -27
- package/skills/yad-pr-template/references/risk-routing.md +97 -24
- package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
- package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
- package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
- package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
- package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
- package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
- package/skills/yad-reconcile/SKILL.md +3 -3
- package/skills/yad-report/SKILL.md +5 -5
- package/skills/yad-review-companion/SKILL.md +12 -9
- package/skills/yad-review-gate/SKILL.md +198 -79
- package/skills/yad-review-gate/references/gating.md +230 -54
- package/skills/yad-run/SKILL.md +86 -56
- package/skills/yad-run/references/run-loop.md +67 -45
- package/skills/yad-ship/SKILL.md +18 -14
- package/skills/yad-spec/SKILL.md +31 -17
- package/skills/yad-spec/references/spec-handoff.md +17 -5
- package/skills/yad-status/SKILL.md +114 -56
- package/skills/yad-stories/SKILL.md +42 -27
- package/skills/yad-stories/references/story-schema.md +10 -9
- package/skills/yad-stub/SKILL.md +59 -48
- package/skills/yad-sync-repos/SKILL.md +3 -3
- package/skills/yad-test-cases/SKILL.md +37 -30
- package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
- package/skills/yad-timeline/SKILL.md +8 -7
- package/skills/yad-ui/SKILL.md +46 -25
- package/cli/roster.mjs +0 -164
- package/skills/sdlc/install.sh +0 -68
package/cli/openpr.mjs
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
// `yad open-pr` — open a code-repo task PR/MR from the repo's platform template (
|
|
1
|
+
// `yad open-pr` — open a code-repo task PR/MR from the repo's platform template (Build).
|
|
2
2
|
// Detects the platform, pushes the current branch, and creates the PR/MR with Summary / Story-task /
|
|
3
|
-
// Impact & Risk prefilled. Distinct from `yad gate open`, which opens a
|
|
4
|
-
// on the
|
|
3
|
+
// Impact & Risk prefilled. Distinct from `yad gate open`, which opens a Shape artifact-review PR
|
|
4
|
+
// on the Product.
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import { c, log, ok, info, warn, hand, fail, run, exists, readJSON } from './lib.mjs';
|
|
8
|
-
import { PROJECT_FILES } from './manifest.mjs';
|
|
8
|
+
import { PROJECT_FILES , productConfigPath } from './manifest.mjs';
|
|
9
9
|
import {
|
|
10
|
-
detectPlatform, createPr,
|
|
10
|
+
detectPlatform, createPr, platformLogin, resolveBaseBranch,
|
|
11
11
|
} from './platform.mjs';
|
|
12
12
|
import { taskFromBranch } from './commit.mjs';
|
|
13
|
-
import { parseReviewBranch, artifactFromBase } from './epic-state.mjs';
|
|
13
|
+
import { parseReviewBranch, artifactFromBase, gateCapFor, capLimit, peopleWord, gateRuleSum, gateRuleEnforced, legacyLogins } from './epic-state.mjs';
|
|
14
|
+
import { activePeople } from './people.mjs';
|
|
15
|
+
import { baseCodeowners, ownersFor, parseCodeowners } from './codeowners.mjs';
|
|
16
|
+
import { baseChangeLevel, changedSince, recentAuthorsFor, suggestedAuthorsFor } from './riskmap-command.mjs';
|
|
17
|
+
import { personLabel } from './riskmap.mjs';
|
|
14
18
|
import { gateOpen } from './gate.mjs';
|
|
15
19
|
|
|
16
20
|
// Resolve the target code repo: --repo <name> from the registry, else --dir, else cwd.
|
|
@@ -23,26 +27,26 @@ function resolveRepo(root, { repo, dir }) {
|
|
|
23
27
|
return { repoRoot: path.resolve(root, dir || '.'), meta: null };
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
// Which SDLC stage is this PR? The
|
|
27
|
-
// `--head` split the
|
|
28
|
-
// code-repo — NOT the
|
|
29
|
-
// hub-
|
|
30
|
-
// hub-tooling — the
|
|
30
|
+
// Which SDLC stage is this PR? The Product serves two vehicles; a code repo only one. Mirrors the
|
|
31
|
+
// `--head` split the Product pattern gates (pr-title.sh/pr-template.sh) already apply:
|
|
32
|
+
// code-repo — NOT the Product (a registry repo via --repo, or root is not a Product).
|
|
33
|
+
// hub-shape — the Product itself AND head is a review/EP-* branch (artifact-review PR).
|
|
34
|
+
// hub-tooling — the Product itself AND head is anything else (a tooling/CI change to the Product).
|
|
31
35
|
// `meta` (truthy when resolved from the repos registry via --repo) is a connected code repo, so it is
|
|
32
|
-
// never the
|
|
36
|
+
// never the Product regardless of its path. Otherwise "is the Product" = repoRoot resolves to root AND root
|
|
33
37
|
// carries .sdlc/hub.json. path.resolve normalises `--dir .` / trailing slashes.
|
|
34
38
|
export function detectStage(root, repoRoot, head, meta) {
|
|
35
39
|
if (meta) return 'code-repo';
|
|
36
40
|
const isHub = path.resolve(repoRoot) === path.resolve(root)
|
|
37
|
-
&& exists(
|
|
41
|
+
&& exists(productConfigPath(root));
|
|
38
42
|
if (!isHub) return 'code-repo';
|
|
39
|
-
return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'hub-
|
|
43
|
+
return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'hub-shape' : 'hub-tooling';
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// The bundled code-task template — the same file `REPO_WIRING` installs into code repos, resolved
|
|
43
47
|
// from the package (mirrors how manifest.mjs reads ../package.json). Used for a hub-tooling PR, whose
|
|
44
48
|
// `.github/pull_request_template.md` is the ARTIFACT-REVIEW template (wrong shape for the code-task
|
|
45
|
-
//
|
|
49
|
+
// Product gate). Falls back to a minimal body that still carries every section the gate requires.
|
|
46
50
|
function codeTaskTemplate(platform) {
|
|
47
51
|
const rel = platform === 'gitlab'
|
|
48
52
|
? '../skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md'
|
|
@@ -65,8 +69,8 @@ function codeTaskTemplate(platform) {
|
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
export function templateBody(repoRoot, platform, { task, summary, risk, contract, domains, stage }) {
|
|
68
|
-
// hub-tooling: the
|
|
69
|
-
// body matches the shape the
|
|
72
|
+
// hub-tooling: the Product's own template is artifact-review — use the bundled code-task template so the
|
|
73
|
+
// body matches the shape the Product `pr-template` gate demands for a non-review head.
|
|
70
74
|
let base;
|
|
71
75
|
if (stage === 'hub-tooling') {
|
|
72
76
|
base = codeTaskTemplate(platform);
|
|
@@ -96,6 +100,134 @@ export function templateBody(repoRoot, platform, { task, summary, risk, contract
|
|
|
96
100
|
return out;
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
// E66 — how many approvers this PR asks for, printed once it is open: the body's own level and contract
|
|
104
|
+
// answer (what `--risk` / `--contract-change` put there), and the risk map on the BASE branch — a change
|
|
105
|
+
// touching a `high` directory adds the high step. The largest step wins, so the map can raise what the
|
|
106
|
+
// body says and never lower it. The same sum `checks/risk-route.sh` prints from the PR body. Printed
|
|
107
|
+
// only, never written: the body keeps the author's level (the user's decision, 2026-09-18).
|
|
108
|
+
export function routeCount(repoRoot, baseBranch, opts = {}) {
|
|
109
|
+
const map = baseChangeLevel(repoRoot, `origin/${baseBranch}`);
|
|
110
|
+
const high = (map.dirs || []).filter((d) => d.level === 'high').map((d) => `${d.dir}${d.state === 'guessed' ? ' (guessed)' : ''}`);
|
|
111
|
+
let riskStep = 0;
|
|
112
|
+
let risk = 'normal';
|
|
113
|
+
if (opts.risk === 'high' || high.length) { riskStep = 1; risk = 'high'; }
|
|
114
|
+
if (opts.contractChange) { riskStep = 2; risk = 'contract'; }
|
|
115
|
+
const rule = { base: 1, riskStep, needed: 1 + riskStep, risk };
|
|
116
|
+
const lines = [];
|
|
117
|
+
if (map.unknown) lines.push([info, `risk map not counted — ${map.unknown}; the count below is the body's alone`]);
|
|
118
|
+
else if (map.noMap) lines.push([info, `risk map: none — ${map.noMap}`]);
|
|
119
|
+
else if (high.length) lines.push([info, `risk map on ${map.base}: high — ${high.join(', ')}`]);
|
|
120
|
+
// E67 — who can meet the ask a `high` directory makes: someone who has committed there in the last 30
|
|
121
|
+
// days, from the BASE branch's history, this change's own authors left out. Reported, never enforced.
|
|
122
|
+
if (high.length) {
|
|
123
|
+
const hist = recentAuthorsFor(repoRoot, `origin/${baseBranch}`, { entries: map.entries, changed: map.changed });
|
|
124
|
+
if (hist.unknown) lines.push([info, `who has worked there lately: not read — ${hist.unknown}`]);
|
|
125
|
+
else if (hist.authors.length) {
|
|
126
|
+
lines.push([hand, `this change asks for an approval from someone who has committed in ${high.join(', ')} in the last 30 days: ${hist.authors.map(personLabel).join(', ')}`]);
|
|
127
|
+
} else lines.push([info, `nobody else has committed in ${high.join(', ')} in the last 30 days — ask anyone who knows it`]);
|
|
128
|
+
}
|
|
129
|
+
if (high.length && opts.risk !== 'high') lines.push([warn, `the body says Risk level: ${opts.risk || 'low'}, but the risk map on ${map.base} marks ${high.join(', ')} high — the larger counts`]);
|
|
130
|
+
// E72 — the cap, SHOWN and never enforced here. The Build count never holds a merge: the platform's
|
|
131
|
+
// branch protection does (Part 9), and the user decided (2026-09-21) that the Build half stays
|
|
132
|
+
// reported even after E108 turns the Shape cap on. That is also E66's answer: the CI half of this count runs from the PR's own checkout of
|
|
133
|
+
// `checks/`, so a PR can edit the script that counts it — harmless only because nothing blocks on it.
|
|
134
|
+
// `gateRuleEnforced` is called WITHOUT the cap on purpose, so the line keeps saying "advisory".
|
|
135
|
+
const cap = gateCapFor(rule, opts.active);
|
|
136
|
+
const capped = cap?.capped ? `, capped to ${cap.to} for ${cap.active} active ${peopleWord(cap.active)}` : '';
|
|
137
|
+
lines.push([riskStep ? hand : info, `this PR asks for ${gateRuleSum(rule)}${capped}${gateRuleEnforced(rule)}; \`bash checks/risk-route.sh "<pr body>" ${map.base}\` prints the count without the cap`]);
|
|
138
|
+
// E71 — how many people are around to meet that ask. `opts.active` is the Product-wide count the
|
|
139
|
+
// CALLER already read: this function runs in a code repo, which has no Product of its own to read
|
|
140
|
+
// (the E65 finding that keeps `checks/*.sh` out of this entirely), so it is handed the number or
|
|
141
|
+
// nothing. `null` means no source could be read, which is never the same as "few people".
|
|
142
|
+
if (opts.active !== undefined) {
|
|
143
|
+
lines.push([info, opts.active === null
|
|
144
|
+
? 'active people: not counted — no cap applies; reported only, and an unreadable source is never read as few people'
|
|
145
|
+
: `active people: ${opts.active} — caps the count at ${capLimit(opts.active)}; reported only, a Build merge is held by branch protection, not by this count`]);
|
|
146
|
+
}
|
|
147
|
+
return { rule, map, lines };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// E68 — a reviewer SUGGESTION, printed once the PR is open. Two separate hints, never merged: the people
|
|
151
|
+
// who have committed in the folders this change touches in the last 30 days (base branch, the change's
|
|
152
|
+
// own authors and robots left out), and what CODEOWNERS on the base lists for those files. It holds
|
|
153
|
+
// nothing, waives nothing and requests no one — E62 decided PRs request no reviewers, and a name here is
|
|
154
|
+
// a hint about who MAY know the code, never a claim about who owns it or who must approve. A git name
|
|
155
|
+
// and a CODEOWNERS login are joined only on exact evidence: a commit address that carries that login.
|
|
156
|
+
export const SUGGEST_SHOWN = 5;
|
|
157
|
+
// The host of a remote URL — `https://host/…`, `ssh://user@host:port/…` or scp-like `user@host:path` —
|
|
158
|
+
// lower-cased, or null.
|
|
159
|
+
export function remoteHost(url) {
|
|
160
|
+
const u = String(url || '').trim();
|
|
161
|
+
const m = u.match(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]*@)?([^/:]+)/i) || u.match(/^(?:[^@/]+@)?([^:/]+):(?!\/\/)/);
|
|
162
|
+
return m ? m[1].toLowerCase() : null;
|
|
163
|
+
}
|
|
164
|
+
// A noreply login is a github.com or gitlab.com account. It is evidence for a CODEOWNERS `@name` only
|
|
165
|
+
// on that same public host: a self-managed GitLab or GitHub Enterprise keeps its own accounts, and
|
|
166
|
+
// one spelled the same may be someone else.
|
|
167
|
+
export const PUBLIC_HOST = { github: 'github.com', gitlab: 'gitlab.com' };
|
|
168
|
+
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
169
|
+
export function suggestReviewers(repoRoot, baseBranch, platform, { remote } = {}) {
|
|
170
|
+
const base = `origin/${baseBranch}`;
|
|
171
|
+
const url = remote ?? run('git', ['remote', 'get-url', 'origin'], { cwd: repoRoot }).stdout;
|
|
172
|
+
const sameHost = remoteHost(url) === PUBLIC_HOST[platform];
|
|
173
|
+
const lines = [];
|
|
174
|
+
const ch = changedSince(repoRoot, base);
|
|
175
|
+
if (ch.unknown) return { lines: [[info, `reviewer suggestion: not read — ${ch.unknown}`]] };
|
|
176
|
+
if (!ch.files.length) return { lines: [[info, `reviewer suggestion: none — this branch changes no file against ${base}`]] };
|
|
177
|
+
|
|
178
|
+
const hist = suggestedAuthorsFor(repoRoot, base, { changed: ch.files });
|
|
179
|
+
const where = hist.folders > 1 ? `the ${hist.folders} folders this change touches` : hist.folders === 1 ? 'the folder this change touches' : 'the folders this change touches';
|
|
180
|
+
const cut = hist.skipped ? ` (${plural(hist.skipped, 'more folder')} not asked about — the list stops at ${hist.folders})` : '';
|
|
181
|
+
const logins = new Set();
|
|
182
|
+
if (hist.unknown) lines.push([info, `who has committed in ${where} lately: not read — ${hist.unknown}`]);
|
|
183
|
+
else if (!hist.authors.length) lines.push([info, `nobody else has committed in ${where} in the last 30 days${cut}`]);
|
|
184
|
+
else {
|
|
185
|
+
// Only a login from THIS platform's noreply address is evidence for a CODEOWNERS `@name` here.
|
|
186
|
+
for (const a of hist.authors) if (a.login && a.loginHost === platform && sameHost) logins.add(a.login);
|
|
187
|
+
const shown = hist.authors.slice(0, SUGGEST_SHOWN).map((a) => `${personLabel(a)} — ${plural(a.commits, 'commit')}`);
|
|
188
|
+
const more = hist.authors.length > SUGGEST_SHOWN ? `, and ${hist.authors.length - SUGGEST_SHOWN} more` : '';
|
|
189
|
+
lines.push([hand, `may know this code — committed in ${where} in the last 30 days: ${shown.join(', ')}${more}${cut}`]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const co = baseCodeowners(repoRoot, base, platform);
|
|
193
|
+
let listed = false;
|
|
194
|
+
if (co.unknown) lines.push([info, `CODEOWNERS: not read — ${co.unknown}`]);
|
|
195
|
+
else if (co.none) lines.push([info, `CODEOWNERS: none — ${co.none}`]);
|
|
196
|
+
else {
|
|
197
|
+
const parsed = parseCodeowners(co.text, platform);
|
|
198
|
+
const got = ownersFor(parsed, ch.files);
|
|
199
|
+
// A file with no owner: no line matched it, the matching line names nobody, or a GitLab `!` line
|
|
200
|
+
// excludes it — the same answer for a reviewer, so one count.
|
|
201
|
+
const unmatched = got.unmatched ? `; ${got.unmatched} of ${plural(ch.files.length, 'touched file')} ${got.unmatched === 1 ? 'has' : 'have'} no owner there` : '';
|
|
202
|
+
// A line this reader skipped could have been the last match for a file, so an answer built without it
|
|
203
|
+
// may be wrong — and "nobody" most of all, since the skipped line may be the only one that matched.
|
|
204
|
+
const skippedN = parsed.skipped.length ? plural(parsed.skipped.length, 'line') : '';
|
|
205
|
+
if (!got.owners.length) {
|
|
206
|
+
lines.push([info, skippedN
|
|
207
|
+
? `CODEOWNERS on ${base} (${co.path}) lists nobody this reader could match for the files this change touches; ${skippedN} could not be read, so this may be wrong`
|
|
208
|
+
: `CODEOWNERS on ${base} (${co.path}) lists nobody for the files this change touches`]);
|
|
209
|
+
} else {
|
|
210
|
+
listed = true;
|
|
211
|
+
const tag = { team: ' (a team)', group: ' (a group)', role: ' (a role)' };
|
|
212
|
+
const shown = got.owners.slice(0, SUGGEST_SHOWN).map((o) => {
|
|
213
|
+
// "the 30-day history", not "above": the person may be one of the `and N more` not shown.
|
|
214
|
+
const also = (o.kind === 'user' || o.kind === 'name') && logins.has(o.text.slice(1)) ? ', also in the 30-day history' : '';
|
|
215
|
+
const notes = [tag[o.kind]?.slice(2, -1), o.optional ? 'optional section' : '', also.slice(2)].filter(Boolean);
|
|
216
|
+
return `${o.text}${notes.length ? ` (${notes.join(', ')})` : ''}`;
|
|
217
|
+
});
|
|
218
|
+
const more = got.owners.length > SUGGEST_SHOWN ? `, and ${got.owners.length - SUGGEST_SHOWN} more` : '';
|
|
219
|
+
const gl = platform === 'gitlab' && got.owners.some((o) => o.kind === 'name') ? '; on GitLab an @name can be a person or a group' : '';
|
|
220
|
+
const partial = skippedN ? `; ${skippedN} could not be read, so this list may be wrong` : '';
|
|
221
|
+
lines.push([hand, `CODEOWNERS on ${base} (${co.path}) lists for the touched files: ${shown.join(', ')}${more}${unmatched}${partial} — a hint only: these files go stale, and yad does not check that anyone listed still works here${gl}`]);
|
|
222
|
+
}
|
|
223
|
+
for (const s of parsed.skipped.slice(0, 3)) lines.push([warn, `CODEOWNERS line ${s.line} not read — ${s.why}`]);
|
|
224
|
+
if (parsed.skipped.length > 3) lines.push([warn, `CODEOWNERS: ${plural(parsed.skipped.length - 3, 'more line')} not read`]);
|
|
225
|
+
}
|
|
226
|
+
const both = listed && hist.authors?.length;
|
|
227
|
+
lines.push([info, `a suggestion only — nobody was asked to review, and no name above is an owner or a required approver${both ? '; the same person can appear in both lists under two names (yad joins them only when a commit address carries the exact @login)' : ''}`]);
|
|
228
|
+
return { lines };
|
|
229
|
+
}
|
|
230
|
+
|
|
99
231
|
export async function runOpenPr(root, opts = {}) {
|
|
100
232
|
log(c.bold('\nyad open-pr'));
|
|
101
233
|
const { repoRoot, meta } = resolveRepo(root, opts);
|
|
@@ -108,11 +240,11 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
108
240
|
const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoRoot }).stdout;
|
|
109
241
|
const stage = detectStage(root, repoRoot, branch, meta);
|
|
110
242
|
|
|
111
|
-
// hub-
|
|
243
|
+
// hub-shape: this is a Shape artifact-review PR (review/EP-*/<artifact> head on the Product). The
|
|
112
244
|
// artifact-review title, body, and ledger bookkeeping all live in `yad gate open` — delegate to it
|
|
113
|
-
// rather than emit the code-task shape (which the
|
|
245
|
+
// rather than emit the code-task shape (which the Product gate would reject). Push first (gateOpen does
|
|
114
246
|
// not push), then hand off; any --title/--message is dropped (gateOpen sets `review: …`).
|
|
115
|
-
if (stage === 'hub-
|
|
247
|
+
if (stage === 'hub-shape') {
|
|
116
248
|
const parsed = parseReviewBranch(branch);
|
|
117
249
|
if (!parsed) { fail(`could not parse review branch '${branch}' (expected review/EP-<slug>/<artifact>)`); process.exitCode = 1; return; }
|
|
118
250
|
info(`pushing ${branch} …`);
|
|
@@ -121,19 +253,19 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
121
253
|
// Pass the branch we just pushed as the head so gateOpen opens the PR against it (its own
|
|
122
254
|
// recompute would collapse a per-story base). gateOpen signals failure by returning no url —
|
|
123
255
|
// mirror open-pr's own error contract so `ship` sees the non-zero exit and never reports success.
|
|
124
|
-
// (On a platform-less
|
|
256
|
+
// (On a platform-less Product gateOpen marks the step in_review locally and returns no url; open-pr's
|
|
125
257
|
// job is to open a PR, so "no PR opened" is a non-zero outcome here, unlike `yad gate open`.)
|
|
126
258
|
const res = await gateOpen(root, { epic: parsed.epic, artifact: artifactFromBase(parsed.base), head: branch });
|
|
127
259
|
if (!res?.url) process.exitCode = 1;
|
|
128
260
|
return res;
|
|
129
261
|
}
|
|
130
262
|
|
|
131
|
-
// The
|
|
132
|
-
// (a hub-tooling branch) — for a connected code repo the
|
|
133
|
-
// must never leak in. Resolved AFTER the hub-
|
|
263
|
+
// The Product's default_branch, which only applies when the PR targets the Product ITSELF
|
|
264
|
+
// (a hub-tooling branch) — for a connected code repo the Product's trunk belongs to a different repo and
|
|
265
|
+
// must never leak in. Resolved AFTER the hub-shape hand-off above, which delegates its own base to
|
|
134
266
|
// `yad gate open`: resolving before it would spend a platform round-trip and print a base that the
|
|
135
267
|
// delegated path then ignores.
|
|
136
|
-
const hub = readJSON(
|
|
268
|
+
const hub = readJSON(productConfigPath(root), {});
|
|
137
269
|
|
|
138
270
|
// Resolve the base rather than assume it (#168). Hardcoding 'main' mis-based every PR on a repo
|
|
139
271
|
// whose trunk is something else — and CodeRabbit decides auto-review eligibility from the base at
|
|
@@ -179,25 +311,45 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
179
311
|
task, summary, risk: opts.risk || 'low', contract: !!opts.contractChange, domains: meta?.name, stage,
|
|
180
312
|
});
|
|
181
313
|
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
const
|
|
186
|
-
const committer = resolveCommitterLogin(repoRoot, roster);
|
|
187
|
-
const scope = meta?.name ? [meta.name] : [];
|
|
188
|
-
// Pass the repo registry entry so a domain owner declared only in repos.json (not the roster roles
|
|
189
|
-
// map) is still requested as a reviewer (BUG-1).
|
|
190
|
-
const reviewers = reviewersForScopes(roster, scope, { excludeLogin: committer, repos: meta ? [meta] : [] });
|
|
314
|
+
// Assignee = the committer: `@me` on GitHub (buildPrArgs sends it when no assignee is named, and `gh`
|
|
315
|
+
// resolves it on the repo's own host), the login `glab` reports on GitLab. No reviewers are requested (E62): the roster that named them is gone. The team requests them
|
|
316
|
+
// on the PR; E68 prints a suggestion from history and CODEOWNERS below, and requests no one.
|
|
317
|
+
const committer = platform === 'gitlab' ? platformLogin(repoRoot, platform) : null;
|
|
191
318
|
const assignees = committer ? [committer] : [];
|
|
192
319
|
|
|
193
320
|
// `creator` is injectable (mirrors gateOpen's) so a test can assert the base that reaches the
|
|
194
321
|
// platform CLI without shelling out to gh/glab.
|
|
195
322
|
const creator = opts.creator || createPr;
|
|
196
|
-
const r = creator(platform, { title, body, base: baseBranch, head: branch,
|
|
323
|
+
const r = creator(platform, { title, body, base: baseBranch, head: branch, assignees, cwd: repoRoot });
|
|
197
324
|
if (!r.ok) { fail(`could not open PR/MR — ${r.reason || 'unknown'}`); process.exitCode = 1; return; }
|
|
198
325
|
ok(`opened ${r.url}`);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
326
|
+
hand('no reviewers were requested — ask them on the PR itself');
|
|
327
|
+
// The Product-wide active count (E71), read ONCE here and handed down: `routeCount` works inside a
|
|
328
|
+
// code repo and must not go looking for a Product itself.
|
|
329
|
+
//
|
|
330
|
+
// ONLY WHEN `root` REALLY IS A PRODUCT. `root` is the working directory, and running this command
|
|
331
|
+
// from inside the code repo is a supported path — `detectStage` returns `code-repo` for exactly that
|
|
332
|
+
// case. Counting from there would have walked the CODE REPO's history and reported its committers as
|
|
333
|
+
// the whole team: no `epics/` (so no ledger and no `unknown`), no `.sdlc/repos.json` (which is a
|
|
334
|
+
// readable "no connected repos"), and one git log that works — a confident, smaller, wrong number,
|
|
335
|
+
// feeding the number E72 caps with. `null` is the honest answer there: we are not standing in a
|
|
336
|
+
// Product, so we did not count.
|
|
337
|
+
const active = (() => {
|
|
338
|
+
// Only the `code-repo` stage prints it (`routeCount`, below). On a `hub-tooling` PR this would
|
|
339
|
+
// otherwise walk the Product and every connected repo's history and throw the answer away.
|
|
340
|
+
if (stage !== 'code-repo') return null;
|
|
341
|
+
if (!exists(productConfigPath(root))) return null;
|
|
342
|
+
// `readJSON`, not `readJSONStrict`, and that is not the degradation cli/people.mjs warns about:
|
|
343
|
+
// losing the roster's name -> login table only stops OLDER records being joined to a login, so a
|
|
344
|
+
// person lands on two rows instead of one. That over-counts, the safe direction here. The count
|
|
345
|
+
// itself still refuses on every source it cannot read.
|
|
346
|
+
try { return activePeople(root, { aliases: legacyLogins(readJSON(productConfigPath(root), null)) }).capacity.active; } catch { return null; }
|
|
347
|
+
})();
|
|
348
|
+
const count = stage === 'code-repo' ? routeCount(repoRoot, baseBranch, { ...opts, active }) : null;
|
|
349
|
+
if (count) for (const [say, line] of count.lines) say(line);
|
|
350
|
+
else if (opts.risk === 'high' || opts.contractChange) hand('high risk / contract surface — run `bash checks/risk-route.sh "<pr body>"` to see how many approvers it asks for');
|
|
351
|
+
if (stage === 'code-repo') for (const [say, line] of suggestReviewers(repoRoot, baseBranch, platform, { remote }).lines) say(line);
|
|
352
|
+
// The --json answer (E1). The count and the reviewer suggestion are said in prose, and a warning among
|
|
353
|
+
// them reaches `warnings`; nothing here is a request — no reviewer is ever asked for (E62).
|
|
354
|
+
return { url: r.url, platform, stage, base: baseBranch, baseSource, branch, title, task: task || null };
|
|
203
355
|
}
|