yadflow 2.7.0 → 2.8.0
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 +2 -8
- package/README.md +8 -4
- package/bin/yad.mjs +15 -1
- package/cli/roster.mjs +164 -0
- package/cli/setup.mjs +128 -2
- package/package.json +2 -3
- package/skills/sdlc/module-help.csv +1 -1
- package/skills/yad-connect-repos/SKILL.md +4 -0
- package/skills/yad-connect-repos/references/hub-config.md +3 -1
- package/skills/yad-docs-overview/SKILL.md +13 -11
- package/skills/yad-docs-overview/references/pipeline-model.md +1 -1
- package/skills/yad-hub-bridge/references/login-roster.md +1 -0
- package/docs/index.html +0 -1323
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
# [2.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
### Bug Fixes
|
|
5
|
-
|
|
6
|
-
* drop unused today param from runDocs (lint) ([1c255f1](https://github.com/abdelrahmannasr/yadflow/commit/1c255f1c848b9571502e29b142a07366612d638c))
|
|
7
|
-
* publish per-epic docs sites in CI + check shell-version staleness ([9862646](https://github.com/abdelrahmannasr/yadflow/commit/9862646fbe910de8ad8248a5f1bb586a5604b18f))
|
|
1
|
+
# [2.8.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.7.0...v2.8.0) (2026-06-15)
|
|
8
2
|
|
|
9
3
|
|
|
10
4
|
### Features
|
|
11
5
|
|
|
12
|
-
* add
|
|
6
|
+
* add `yad roster` command to manage the reviewer roster any time ([#64](https://github.com/abdelrahmannasr/yadflow/issues/64)) ([4d78225](https://github.com/abdelrahmannasr/yadflow/commit/4d78225ec25579b50d24d917f217212f4820728f))
|
|
13
7
|
|
|
14
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
15
9
|
|
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ with `npx` from your **product hub** repo — no clone needed.
|
|
|
90
90
|
| `npx yadflow check --fix` | Reconcile: fill what is missing **and** update what changed — touches nothing already correct. |
|
|
91
91
|
| `npx yadflow update` | Apply drift only (alias for `check --fix --scope=changed`). Also migrates a pre-2.0 install in place: `sdlc-*` skill copies and marker-owned `sdlc-*.yml` CI files are replaced by their `yad-*` names (a same-named file *you* authored is never touched). |
|
|
92
92
|
| `npx yadflow doctor [--json]` | Environment + state health: tools on PATH and platform auth, config files parse and point at real repos, every epic ledger loads. Exit 1 on any failure; `--json` for CI and bug reports. |
|
|
93
|
+
| `yad roster list` / `yad roster add <login>` | Manage the reviewer roster + per-repo roles **any time** (not just at setup). `add` upserts a member then walks each connected repo asking for their role; `grant`/`revoke <name> <repo> <role>` and `remove <login>` round it out. A `domain-owner` grant keeps `repos.json` `domain_owners` in sync. |
|
|
93
94
|
| `yad gate open <epic> <artifact>` | Open the front-half **review PR/MR** for an artifact and mark the step `in_review`. |
|
|
94
95
|
| `yad gate sync <epic> [artifact]` | Pull the PR/MR's reviews + comment threads into the file ledger; **auto-advance** the step when approvals are satisfied, all threads are resolved, and the PR is merged. |
|
|
95
96
|
| `yad gate comments <epic> [artifact]` | Fetch the unresolved review comments to address (then reply on the PR; reviewers resolve their threads). |
|
|
@@ -136,6 +137,7 @@ simultaneous advancements can be lost; the next event or scheduled sweep re-sync
|
|
|
136
137
|
2. **Install the module** — copy all 29 `yad-*` skills into the IDE skill dirs you pick
|
|
137
138
|
(`.claude/`, `.agents/`, `.zencoder/`, `.opencode/`) and register `_bmad/sdlc/`.
|
|
138
139
|
3. **Hub platform & roster** — detect GitHub/GitLab from the remote; record reviewers → `.sdlc/hub.json`.
|
|
140
|
+
Edit the roster any time afterwards with `yad roster` (no need to re-run the whole wizard).
|
|
139
141
|
4. **Connect a design tool** — record the design tool (Figma / pencil / none) → `.sdlc/design.json` so
|
|
140
142
|
the UI step can materialize the design; the MCP itself is confirmed later by `yad-connect-design`.
|
|
141
143
|
5. **Connect a testing tool** — record the testing tool (Playwright / cypress / pytest / none) →
|
|
@@ -234,7 +236,8 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
|
|
|
234
236
|
contract lock. `generate` / `refresh` / `deploy`.
|
|
235
237
|
- **`yad-docs-overview`** — Generates the project **SDLC-overview site** (`docs/sdlc-site/`) — every
|
|
236
238
|
stage from setup → ship as flow paths / system components / stakeholder roles, reusing the same shell —
|
|
237
|
-
superseding the hand-maintained `docs/index.html
|
|
239
|
+
superseding the hand-maintained `docs/index.html` (folded into the site as `public/report.html`, linked
|
|
240
|
+
from the nav).
|
|
238
241
|
- **`yad-docs-sync`** — Keeps the sites fresh: detects staleness (a content hash of the authored
|
|
239
242
|
artifacts + the connected repos' HEAD shas vs each site's build manifest), regenerates + redeploys, and
|
|
240
243
|
can wire a CI job that rebuilds on push. Generalizes the rule that feature work must hand-update the
|
|
@@ -387,9 +390,10 @@ detailed sections below expand every phase. Invoke a skill by name in your agent
|
|
|
387
390
|
`testing.json`, lets `yad-test-cases` implement automation), `yad-connect-learning action: connect`
|
|
388
391
|
(DeepTutor-first → `learning.json`, powers the cross-cutting learning layer).
|
|
389
392
|
7. **(Optional) Put the hub on a platform** so the front-half review runs through real PRs:
|
|
390
|
-
`yad-connect-repos action: detect-hub`, then `
|
|
391
|
-
name +
|
|
392
|
-
|
|
393
|
+
`yad-connect-repos action: detect-hub`, then `yad roster add <login>` once per reviewer (login →
|
|
394
|
+
SDLC name + per-repo roles — the `add` walk asks for each connected repo's role; `yad roster grant`
|
|
395
|
+
sets one directly), and `yad-pr-template repo:hub action: wire` / `yad-review-comments repo:hub
|
|
396
|
+
action: wire` / `yad-checks repo:hub action: wire`. With no hub platform the front gate runs file-only.
|
|
393
397
|
8. **Conventions:** commits and PR/MR titles follow Conventional Commits (lowercase after the type), the
|
|
394
398
|
human author owns each commit with an optional per-commit `Co-Authored-By` AI trailer — see
|
|
395
399
|
[`CONTRIBUTING.md`](CONTRIBUTING.md).
|
package/bin/yad.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { runCommit } from '../cli/commit.mjs';
|
|
|
10
10
|
import { runOpenPr } from '../cli/openpr.mjs';
|
|
11
11
|
import { runShip } from '../cli/ship.mjs';
|
|
12
12
|
import { runRepo } from '../cli/repo.mjs';
|
|
13
|
+
import { runRoster } from '../cli/roster.mjs';
|
|
13
14
|
import { runDocs } from '../cli/docs.mjs';
|
|
14
15
|
import { runDoctor } from '../cli/doctor.mjs';
|
|
15
16
|
|
|
@@ -24,6 +25,14 @@ ${c.bold('Setup & maintenance')}
|
|
|
24
25
|
yad doctor [--json] Environment + state health: tools/auth, config files,
|
|
25
26
|
repo paths, epic ledgers (exit 1 on any failure)
|
|
26
27
|
|
|
28
|
+
${c.bold('Reviewer roster')}
|
|
29
|
+
yad roster list Show every member + their roles per scope (hub + each repo)
|
|
30
|
+
yad roster add <login> Add/edit a member, then walk the connected repos for their roles
|
|
31
|
+
(--name, --email, --roles "hub=owner,reviewer backend=domain-owner")
|
|
32
|
+
yad roster grant <name> <repo> <role...> Grant role(s) for a connected repo (domain-owner|reviewer|owner)
|
|
33
|
+
yad roster revoke <name> <repo> <role...> Remove role(s) for a repo
|
|
34
|
+
yad roster remove <login> Delete a member from the roster
|
|
35
|
+
|
|
27
36
|
${c.bold('Review gate (front half)')}
|
|
28
37
|
yad gate open <epic> <artifact> Open the review PR/MR; mark the step in_review
|
|
29
38
|
yad gate sync <epic> [artifact] Pull PR state -> ledger; advance on approved+resolved+merged
|
|
@@ -66,7 +75,7 @@ ${c.bold('Options')}
|
|
|
66
75
|
-h, --help Show this help
|
|
67
76
|
-v, --version Print version`;
|
|
68
77
|
|
|
69
|
-
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic']);
|
|
78
|
+
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--name', '--email', '--roles']);
|
|
70
79
|
|
|
71
80
|
function parseArgs(argv) {
|
|
72
81
|
const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
|
|
@@ -147,6 +156,11 @@ async function main() {
|
|
|
147
156
|
await runRepo(o.dir, { action: action || 'list', name, today });
|
|
148
157
|
break;
|
|
149
158
|
}
|
|
159
|
+
case 'roster': {
|
|
160
|
+
const [, action, ...rest] = o._;
|
|
161
|
+
await runRoster(o.dir, { action: action || 'list', args: rest, name: o.name, email: o.email, roles: o.roles, today });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
150
164
|
case 'docs': {
|
|
151
165
|
const [, action] = o._;
|
|
152
166
|
if (o.epic && !isValidEpicId(o.epic)) { log(c.red(`invalid epic id: ${o.epic} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
|
package/cli/roster.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// `yad roster` — manage the reviewer roster (.sdlc/hub.json) and per-repo roles at any time.
|
|
2
|
+
// The roster maps a platform login -> SDLC name + a per-scope roles map; it is the only thing that lets
|
|
3
|
+
// the review gate attribute approvals and route per-repo domain-owner reviewers. This is the standalone
|
|
4
|
+
// counterpart of the `yad setup` roster step and the `yad-connect-repos action: roster` skill action.
|
|
5
|
+
// Repo-driven: `add`/`edit` walks the connected repos from repos.json so roles are assigned against what
|
|
6
|
+
// is actually connected, not against repo names the user has to remember. Granting/revoking a
|
|
7
|
+
// `domain-owner` keeps repos.json `domain_owners` in sync so the gate never drifts from the roster.
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { c, log, ok, info, warn, hand, fail, ask, askYesNo, readJSON, writeJSON } from './lib.mjs';
|
|
10
|
+
import { PROJECT_FILES } from './manifest.mjs';
|
|
11
|
+
import { rolesForScope } from './platform.mjs';
|
|
12
|
+
import { parseRolesSpec, upsertRosterEntry, addRepoRoles, removeRepoRole, setRepoDomainOwners, reconcileRepoRoles } from './setup.mjs';
|
|
13
|
+
|
|
14
|
+
const ROLES = ['owner', 'reviewer', 'domain-owner'];
|
|
15
|
+
|
|
16
|
+
const loadHub = (root) => readJSON(path.join(root, PROJECT_FILES.hubConfig), null);
|
|
17
|
+
const loadRepos = (root) => readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] }).repos || [];
|
|
18
|
+
const ownersOf = (repo) => (Array.isArray(repo.domain_owners) ? repo.domain_owners : (repo.domain_owner ? [repo.domain_owner] : []));
|
|
19
|
+
|
|
20
|
+
// `list` — every member with their per-scope roles, plus a drift check between hub.json roles and
|
|
21
|
+
// repos.json domain_owners (the two should agree; the sync on grant/revoke keeps them aligned).
|
|
22
|
+
function rosterList(root) {
|
|
23
|
+
const hub = loadHub(root);
|
|
24
|
+
if (!hub || !Array.isArray(hub.roster) || !hub.roster.length) {
|
|
25
|
+
warn('no roster yet (.sdlc/hub.json) — add one with `yad roster add <login>` or `yad setup`');
|
|
26
|
+
return { members: 0 };
|
|
27
|
+
}
|
|
28
|
+
const repos = loadRepos(root);
|
|
29
|
+
log(c.bold('\nreviewer roster'));
|
|
30
|
+
for (const e of hub.roster) {
|
|
31
|
+
const flag = e.unverified ? c.yellow(' (unverified)') : '';
|
|
32
|
+
log(` ${c.bold(e.name || e.login)} ${c.dim(`@${e.login}`)}${e.email ? c.dim(` <${e.email}>`) : ''}${flag}`);
|
|
33
|
+
const hubRoles = rolesForScope(e, 'hub');
|
|
34
|
+
log(` hub: ${hubRoles.length ? hubRoles.join(', ') : c.dim('—')}`);
|
|
35
|
+
for (const r of repos) {
|
|
36
|
+
const rr = rolesForScope(e, r.name);
|
|
37
|
+
if (rr.length) log(` ${r.name}: ${rr.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const drift = [];
|
|
41
|
+
for (const r of repos) {
|
|
42
|
+
const owners = ownersOf(r);
|
|
43
|
+
for (const nm of owners) {
|
|
44
|
+
const e = hub.roster.find((x) => x.name === nm);
|
|
45
|
+
if (!e || !rolesForScope(e, r.name).includes('domain-owner')) {
|
|
46
|
+
drift.push(`${nm} owns ${r.name} in repos.json but has no domain-owner role in hub.json`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const e of hub.roster) {
|
|
50
|
+
if (rolesForScope(e, r.name).includes('domain-owner') && !owners.includes(e.name)) {
|
|
51
|
+
drift.push(`${e.name} has domain-owner for ${r.name} in hub.json but is missing from repos.json domain_owners`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (drift.length) { log(''); for (const d of drift) warn(d); }
|
|
56
|
+
return { members: hub.roster.length, repos: repos.length, drift: drift.length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The repo-driven walk: for each connected repo show the member's current role and offer to set it.
|
|
60
|
+
async function repoWalk(root, entry) {
|
|
61
|
+
const repos = loadRepos(root);
|
|
62
|
+
if (!repos.length) { info('no connected repos to assign roles for (.sdlc/repos.json) — connect one with `yad setup`'); return; }
|
|
63
|
+
for (const r of repos) {
|
|
64
|
+
const cur = rolesForScope(entry, r.name);
|
|
65
|
+
if (!(await askYesNo(` set ${entry.name}'s role on ${r.name}? (current: ${cur.length ? cur.join(', ') : 'none'})`, false))) continue;
|
|
66
|
+
const input = await ask(' roles (domain-owner/reviewer/owner, space-separated; blank = clear)', cur.join(' '));
|
|
67
|
+
const want = input.split(/\s+/).map((x) => x.trim()).filter(Boolean);
|
|
68
|
+
const invalid = want.filter((x) => !ROLES.includes(x));
|
|
69
|
+
if (invalid.length) warn(`ignoring unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`);
|
|
70
|
+
reconcileRepoRoles(root, entry.name, r.name, cur, want.filter((x) => ROLES.includes(x)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Mirror any domain-owner scopes from a non-interactive `--roles` upsert into repos.json.
|
|
75
|
+
function syncDomainOwners(root, entry) {
|
|
76
|
+
for (const [scope, list] of Object.entries(entry.roles || {})) {
|
|
77
|
+
if (scope !== 'hub' && Array.isArray(list) && list.includes('domain-owner')) setRepoDomainOwners(root, scope, entry.name, { add: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `add`/`edit <login>` — upsert by login (from flags or interactive prompts), then either apply a
|
|
82
|
+
// `--roles` spec directly (scriptable) or run the repo-driven walk (interactive default).
|
|
83
|
+
async function rosterAdd(root, login, { name, email, roles } = {}) {
|
|
84
|
+
if (!login) { fail('usage: yad roster add <login> [--name N] [--email E] [--roles "hub=owner,reviewer backend=domain-owner"]'); process.exitCode = 1; return {}; }
|
|
85
|
+
const hub = loadHub(root);
|
|
86
|
+
const platform = hub ? hub.platform : null;
|
|
87
|
+
const existing = hub && Array.isArray(hub.roster) ? hub.roster.find((e) => e.login === login) : null;
|
|
88
|
+
const scripted = !!roles;
|
|
89
|
+
let nm = name;
|
|
90
|
+
let em = email;
|
|
91
|
+
let rolesMap;
|
|
92
|
+
if (scripted) {
|
|
93
|
+
rolesMap = parseRolesSpec(roles);
|
|
94
|
+
} else {
|
|
95
|
+
nm = nm || await ask(' yad name', (existing && existing.name) || login);
|
|
96
|
+
em = em || await ask(' commit email (blank to skip)', (existing && existing.email) || '');
|
|
97
|
+
const def = rolesForScope(existing, 'hub').join(' ') || 'reviewer';
|
|
98
|
+
const hubRoles = (await ask(' hub roles (owner/reviewer, space-separated)', def)).split(/\s+/).filter(Boolean);
|
|
99
|
+
rolesMap = hubRoles.length ? { hub: hubRoles } : {};
|
|
100
|
+
}
|
|
101
|
+
const { entry, created } = upsertRosterEntry(root, { login, name: nm, email: em || undefined, roles: rolesMap, platform });
|
|
102
|
+
if (!entry) return {};
|
|
103
|
+
ok(`${created ? 'added' : 'updated'} ${entry.name} (@${login})`);
|
|
104
|
+
if (scripted) syncDomainOwners(root, entry);
|
|
105
|
+
else await repoWalk(root, entry);
|
|
106
|
+
return { entry: entry.name, created };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `grant <name> <repo> <role...>` — scriptable per-repo grant (member must already be in the roster).
|
|
110
|
+
function rosterGrant(root, [name, repo, ...roles]) {
|
|
111
|
+
if (!name || !repo || !roles.length) { fail('usage: yad roster grant <name> <repo> <role...>'); process.exitCode = 1; return {}; }
|
|
112
|
+
const invalid = roles.filter((r) => !ROLES.includes(r));
|
|
113
|
+
if (invalid.length) { fail(`unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`); process.exitCode = 1; return {}; }
|
|
114
|
+
const hub = loadHub(root);
|
|
115
|
+
if (!hub || !Array.isArray(hub.roster) || !hub.roster.some((e) => e.name === name)) {
|
|
116
|
+
fail(`'${name}' is not in the roster — add them first with \`yad roster add <login>\``); process.exitCode = 1; return {};
|
|
117
|
+
}
|
|
118
|
+
if (!loadRepos(root).some((r) => r.name === repo)) warn(`repo '${repo}' is not registered — the role is recorded and applies once it is connected`);
|
|
119
|
+
addRepoRoles(root, repo, Object.fromEntries(roles.map((r) => [r, [name]])));
|
|
120
|
+
if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: true });
|
|
121
|
+
ok(`granted ${name} ${roles.join(', ')} on ${repo}`);
|
|
122
|
+
return { name, repo, roles };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// `revoke <name> <repo> <role...>` — scriptable per-repo revoke.
|
|
126
|
+
function rosterRevoke(root, [name, repo, ...roles]) {
|
|
127
|
+
if (!name || !repo || !roles.length) { fail('usage: yad roster revoke <name> <repo> <role...>'); process.exitCode = 1; return {}; }
|
|
128
|
+
removeRepoRole(root, name, repo, roles);
|
|
129
|
+
if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: false });
|
|
130
|
+
ok(`revoked ${name} ${roles.join(', ')} on ${repo}`);
|
|
131
|
+
return { name, repo, roles };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// `remove <login>` — delete a member; warn (do not cascade) if still a domain owner in repos.json.
|
|
135
|
+
function rosterRemove(root, login) {
|
|
136
|
+
if (!login) { fail('usage: yad roster remove <login>'); process.exitCode = 1; return {}; }
|
|
137
|
+
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
138
|
+
const hub = readJSON(hubPath, null);
|
|
139
|
+
if (!hub || !Array.isArray(hub.roster)) { warn('no roster to remove from (.sdlc/hub.json)'); return { removed: 0 }; }
|
|
140
|
+
const idx = hub.roster.findIndex((e) => e.login === login);
|
|
141
|
+
if (idx < 0) { warn(`no roster member with login '${login}'`); return { removed: 0 }; }
|
|
142
|
+
const [removed] = hub.roster.splice(idx, 1);
|
|
143
|
+
writeJSON(hubPath, hub);
|
|
144
|
+
ok(`removed ${removed.name} (@${login})`);
|
|
145
|
+
const refs = loadRepos(root).filter((r) => ownersOf(r).includes(removed.name)).map((r) => r.name);
|
|
146
|
+
if (refs.length) hand(`'${removed.name}' is still a domain owner in repos.json for: ${refs.join(', ')} — revoke with \`yad roster revoke ${removed.name} <repo> domain-owner\``);
|
|
147
|
+
return { removed: 1 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function runRoster(root, { action = 'list', args = [], name, email, roles } = {}) {
|
|
151
|
+
switch (action) {
|
|
152
|
+
case 'list': return rosterList(root);
|
|
153
|
+
case 'add':
|
|
154
|
+
case 'edit': return rosterAdd(root, args[0], { name, email, roles });
|
|
155
|
+
case 'grant': return rosterGrant(root, args);
|
|
156
|
+
case 'revoke': return rosterRevoke(root, args);
|
|
157
|
+
case 'remove':
|
|
158
|
+
case 'rm': return rosterRemove(root, args[0]);
|
|
159
|
+
default:
|
|
160
|
+
fail(`unknown roster action: ${action} (list | add | edit | grant | revoke | remove)`);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
package/cli/setup.mjs
CHANGED
|
@@ -11,13 +11,29 @@ import {
|
|
|
11
11
|
moduleActions, repoActions, hubActions, authorsActions,
|
|
12
12
|
legacyModuleActions, legacyRepoActions, legacyHubActions,
|
|
13
13
|
} from './plan.mjs';
|
|
14
|
-
import { validateLogin } from './platform.mjs';
|
|
14
|
+
import { validateLogin, rolesForScope } from './platform.mjs';
|
|
15
15
|
|
|
16
16
|
// Parse a comma/space separated list into a clean, deduped array of trimmed tokens.
|
|
17
|
-
function parseList(s) {
|
|
17
|
+
export function parseList(s) {
|
|
18
18
|
return [...new Set((s || '').split(/[,\s]+/).map((x) => x.trim()).filter(Boolean))];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Parse a per-scope roles spec — `"hub=owner,reviewer backend=domain-owner"` — into the roster's
|
|
22
|
+
// per-scope map `{ hub: ['owner','reviewer'], backend: ['domain-owner'] }`. Tokens are whitespace
|
|
23
|
+
// separated; each is `scope=role[,role...]`. Malformed tokens (no `=`, empty scope/roles) are skipped.
|
|
24
|
+
export function parseRolesSpec(s) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const tok of (s || '').split(/\s+/).map((x) => x.trim()).filter(Boolean)) {
|
|
27
|
+
const eq = tok.indexOf('=');
|
|
28
|
+
if (eq < 0) continue;
|
|
29
|
+
const scope = tok.slice(0, eq).trim();
|
|
30
|
+
const roles = parseList(tok.slice(eq + 1));
|
|
31
|
+
if (!scope || !roles.length) continue;
|
|
32
|
+
out[scope] = [...new Set([...(out[scope] || []), ...roles])];
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
const ALL_IDES = [...IDE_FOLDER_TARGETS, '.opencode'];
|
|
22
38
|
|
|
23
39
|
export function detectPlatform(remoteUrl = '') {
|
|
@@ -68,6 +84,98 @@ export function addRepoRoles(root, repo, grants = {}) {
|
|
|
68
84
|
if (touched) writeJSON(hubPath, hub);
|
|
69
85
|
}
|
|
70
86
|
|
|
87
|
+
// Normalize a roster entry's roles in place to the per-scope map, migrating the two legacy shapes
|
|
88
|
+
// (a flat array, or a single `role` string) into `roles.hub` so nothing is lost. Mirrors the
|
|
89
|
+
// migration addRepoRoles does; pulled out so upsert/remove share it.
|
|
90
|
+
function normalizeRoles(entry) {
|
|
91
|
+
if (!entry.roles || typeof entry.roles !== 'object' || Array.isArray(entry.roles)) {
|
|
92
|
+
const hub = Array.isArray(entry.roles) ? entry.roles : (entry.role ? [entry.role] : []);
|
|
93
|
+
entry.roles = hub.length ? { hub } : {};
|
|
94
|
+
delete entry.role;
|
|
95
|
+
}
|
|
96
|
+
return entry.roles;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Upsert one roster member into hub.json, keyed by `login`. Deep-merges the per-scope `roles` map so
|
|
100
|
+
// scopes the caller did not name are preserved; sets `name`/`email` when given; validates the login
|
|
101
|
+
// against the hub (warn-only — a miss flags `unverified`, `checked:false` skips silently). Creates the
|
|
102
|
+
// hub.json shell if absent. Returns { entry, created }.
|
|
103
|
+
export function upsertRosterEntry(root, { login, name, email, roles = {}, platform } = {}) {
|
|
104
|
+
if (!login) { warn('roster upsert needs a login — skipped'); return { entry: null, created: false }; }
|
|
105
|
+
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
106
|
+
const hub = readJSON(hubPath, null) || { platform: platform && platform !== 'none' ? platform : null, bridge_enabled: false, bridge: false, default_branch: 'main', roster: [] };
|
|
107
|
+
if (!Array.isArray(hub.roster)) hub.roster = [];
|
|
108
|
+
let entry = hub.roster.find((e) => e.login === login);
|
|
109
|
+
const created = !entry;
|
|
110
|
+
if (!entry) { entry = { login, name: name || login, roles: {} }; hub.roster.push(entry); }
|
|
111
|
+
if (name) entry.name = name;
|
|
112
|
+
if (email) entry.email = email;
|
|
113
|
+
normalizeRoles(entry);
|
|
114
|
+
for (const [scope, list] of Object.entries(roles || {})) {
|
|
115
|
+
const cur = Array.isArray(entry.roles[scope]) ? entry.roles[scope] : [];
|
|
116
|
+
entry.roles[scope] = [...new Set([...cur, ...list])];
|
|
117
|
+
}
|
|
118
|
+
if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) warn(`'${email}' does not look like an email address`);
|
|
119
|
+
const plat = platform || hub.platform;
|
|
120
|
+
if (plat && plat !== 'none') {
|
|
121
|
+
const v = validateLogin(plat, login);
|
|
122
|
+
if (v.checked && !v.exists) { warn(`'${login}' not found on ${plat} — saved as unverified`); entry.unverified = true; }
|
|
123
|
+
else if (v.checked && v.exists) { ok(`verified ${login} on ${plat}`); delete entry.unverified; }
|
|
124
|
+
}
|
|
125
|
+
writeJSON(hubPath, hub);
|
|
126
|
+
return { entry, created };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Inverse of addRepoRoles: drop the named role(s) from a member's `roles[<repo>]` scope, removing the
|
|
130
|
+
// scope key when it empties. Member is found by yad `name` (matching addRepoRoles). Idempotent.
|
|
131
|
+
export function removeRepoRole(root, name, repo, roles = []) {
|
|
132
|
+
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
133
|
+
const hub = readJSON(hubPath, null);
|
|
134
|
+
if (!hub || !Array.isArray(hub.roster)) return;
|
|
135
|
+
const entry = hub.roster.find((e) => e.name === name);
|
|
136
|
+
if (!entry) { warn(`'${name}' is not in the roster — nothing to revoke for ${repo}`); return; }
|
|
137
|
+
normalizeRoles(entry);
|
|
138
|
+
const cur = Array.isArray(entry.roles[repo]) ? entry.roles[repo] : [];
|
|
139
|
+
const next = cur.filter((r) => !roles.includes(r));
|
|
140
|
+
if (next.length === cur.length) return; // nothing removed
|
|
141
|
+
if (next.length) entry.roles[repo] = next; else delete entry.roles[repo];
|
|
142
|
+
writeJSON(hubPath, hub);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Keep repos.json `domain_owners` in sync when a domain-owner role is granted/revoked via the roster,
|
|
146
|
+
// so the gate's per-repo reviewer-routing and the derivation fallback never drift from hub.json. Adds
|
|
147
|
+
// or removes the yad `name` and mirrors `domain_owner = domain_owners[0]`. No-op + warn if the repo is
|
|
148
|
+
// not registered. Returns true when the registry was written.
|
|
149
|
+
export function setRepoDomainOwners(root, repo, name, { add = true } = {}) {
|
|
150
|
+
const regPath = path.join(root, PROJECT_FILES.reposRegistry);
|
|
151
|
+
const registry = readJSON(regPath, { repos: [] });
|
|
152
|
+
const entry = (registry.repos || []).find((r) => r.name === repo);
|
|
153
|
+
if (!entry) { warn(`repo '${repo}' is not registered (.sdlc/repos.json) — domain_owners not synced`); return false; }
|
|
154
|
+
const owners = Array.isArray(entry.domain_owners) ? [...entry.domain_owners] : (entry.domain_owner ? [entry.domain_owner] : []);
|
|
155
|
+
const has = owners.includes(name);
|
|
156
|
+
let next;
|
|
157
|
+
if (add) { if (has) return false; next = [...owners, name]; }
|
|
158
|
+
else { if (!has) return false; next = owners.filter((o) => o !== name); }
|
|
159
|
+
entry.domain_owners = next;
|
|
160
|
+
entry.domain_owner = next[0] || '';
|
|
161
|
+
writeJSON(regPath, registry);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reconcile one repo's roles for a member to exactly `want`: grant what is new, revoke what is gone,
|
|
166
|
+
// and mirror domain-owner changes into repos.json. `current` is the member's existing roles for the
|
|
167
|
+
// repo. Shared by the `yad roster` walk and the `yad setup` per-repo role step. Idempotent.
|
|
168
|
+
export function reconcileRepoRoles(root, name, repo, current = [], want = []) {
|
|
169
|
+
const toAdd = want.filter((r) => !current.includes(r));
|
|
170
|
+
const toRemove = current.filter((r) => !want.includes(r));
|
|
171
|
+
if (!toAdd.length && !toRemove.length) { info(` ${repo}: unchanged`); return; }
|
|
172
|
+
if (toAdd.length) addRepoRoles(root, repo, Object.fromEntries(toAdd.map((r) => [r, [name]])));
|
|
173
|
+
if (toRemove.length) removeRepoRole(root, name, repo, toRemove);
|
|
174
|
+
if (toAdd.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: true });
|
|
175
|
+
if (toRemove.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: false });
|
|
176
|
+
ok(` ${repo}: ${want.length ? want.join(', ') : 'cleared'}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
71
179
|
export function registerRepo(root, registry, { name, rpath, platform, domain_owner = '', domain_owners = null, default_branch = 'main', today = null }) {
|
|
72
180
|
if (!insideRoot(root, rpath)) {
|
|
73
181
|
warn(`${rpath} resolves outside the project root — skipped`);
|
|
@@ -370,6 +478,24 @@ export async function runSetup(root, opts = {}) {
|
|
|
370
478
|
}
|
|
371
479
|
}
|
|
372
480
|
|
|
481
|
+
// 7b. Assign/update roles for ALREADY-connected repos. The connect loop above only prompts for the
|
|
482
|
+
// repos you add now; this closes the gap so a member's domain-owner/reviewer/owner role on a repo
|
|
483
|
+
// connected in an earlier run can be set without reconnecting. Mirrors `yad roster` (repo-driven).
|
|
484
|
+
const hub7 = readJSON(hubPath, null);
|
|
485
|
+
if (registry.repos.length && hub7 && Array.isArray(hub7.roster) && hub7.roster.length
|
|
486
|
+
&& await askYesNo('Assign/update roles for connected repos?', false)) {
|
|
487
|
+
for (const member of hub7.roster) {
|
|
488
|
+
if (!(await askYesNo(` edit ${member.name}'s repo roles?`, false))) continue;
|
|
489
|
+
for (const repo of registry.repos) {
|
|
490
|
+
const cur = rolesForScope(member, repo.name);
|
|
491
|
+
if (!(await askYesNo(` set ${member.name}'s role on ${repo.name}? (current: ${cur.length ? cur.join(', ') : 'none'})`, false))) continue;
|
|
492
|
+
const input = await ask(' roles (domain-owner/reviewer/owner, space-separated; blank = clear)', cur.join(' '));
|
|
493
|
+
const want = parseList(input).filter((x) => ['owner', 'reviewer', 'domain-owner'].includes(x));
|
|
494
|
+
reconcileRepoRoles(root, member.name, repo.name, cur, want);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
373
499
|
// 8. Wire each connected repo + the hub itself
|
|
374
500
|
step(8, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
|
|
375
501
|
if (registry.repos.length === 0) info('no repos to wire');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 29 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"!cli/test.mjs",
|
|
23
23
|
"!cli/test-checks.mjs",
|
|
24
24
|
"skills/",
|
|
25
|
-
"docs/index.html",
|
|
26
25
|
"README.md",
|
|
27
26
|
"LICENSE",
|
|
28
27
|
"CHANGELOG.md"
|
|
@@ -55,7 +54,7 @@
|
|
|
55
54
|
"deeptutor"
|
|
56
55
|
],
|
|
57
56
|
"devDependencies": {
|
|
58
|
-
"@eslint/js": "^
|
|
57
|
+
"@eslint/js": "^10.0.1",
|
|
59
58
|
"@semantic-release/changelog": "^6.0.3",
|
|
60
59
|
"eslint": "^9.39.4",
|
|
61
60
|
"semantic-release": "^25.0.3"
|
|
@@ -26,5 +26,5 @@ SDLC Workflow,yad-run,Run (Automation),RN,"Phase 4 orchestrator: drive a story's
|
|
|
26
26
|
SDLC Workflow,yad-status,SDLC Status,SS,"Read-only: print the full front-state chain, per-step dials, contract lock, story repo tags, and pending approvals at the active gate. For stories in the build half, also print each back-half step's automation dial and status, the trust record (runs / % approved-unchanged / earned vs gathering evidence), and the system-wide kill-switch state.",,{epic: EP-<slug>},1-front,,,false,,
|
|
27
27
|
SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connect a docs/Pages publishing target so the interactive-docs steps can deploy the generated site (not just commit its source). Auto-detects the platform from .sdlc/hub.json (github->github-pages, gitlab->gitlab-pages, null->build-only), resolves the Vite base path, and records the target in .sdlc/docs.json (local-user auth, no stored tokens). Degrades to build-only when gh/glab is absent. Idempotent and refreshable; one connection per project. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {target: github-pages|gitlab-pages|none} {scope: hub|<repo>|dedicated} {base_path: <override>},0-setup,,yad-docs,false,.sdlc/,docs.json
|
|
28
28
|
SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive documentation SPA (animated flow canvas + role-based stakeholder doc pages) from the epic's approved artifacts (epic, architecture, locked contract, ui-design, stories, code-context, test-cases), themed by the connected design system, into epics/EP-<slug>/docs-site/. Copies the vendored React/Vite/Tailwind shell verbatim and generates src/data/*.ts deterministically; drives the yad docs build/deploy CLI to publish to Pages (or build-only). An OUTPUT ENRICHMENT — never a gate; never mutates state.json, approvals, or the contract lock.",,{epic: EP-<slug>} {action: generate|refresh|deploy} {login_gate: true|false},,yad-review-gate,,false,epics/EP-<slug>/docs-site/,docs-site/ docs-build.json
|
|
29
|
-
SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source.
|
|
29
|
+
SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
|
|
30
30
|
SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
|
|
@@ -133,6 +133,10 @@ write only `{project-root}/.sdlc/hub.json` (`config.yaml` `hub.config`) — neve
|
|
|
133
133
|
`roles[<repo>]` (and still **derived** as a fallback when a roster `name` equals a repo's
|
|
134
134
|
`domain_owner`/`domain_owners` in `repos.json` — see `references/hub-config.md`). An unmapped login
|
|
135
135
|
degrades to a plain `reviewer`, never auto-promoted to owner/domain-owner.
|
|
136
|
+
**The deterministic half is the `yad roster` CLI command** — runnable any time, not just at setup:
|
|
137
|
+
`yad roster list`; `yad roster add <login>` (upsert, then a repo-driven walk that asks for each
|
|
138
|
+
connected repo's role); `yad roster grant|revoke <name> <repo> <role>`; `yad roster remove <login>`.
|
|
139
|
+
A `domain-owner` grant/revoke keeps `repos.json` `domain_owners` in sync so the gate never drifts.
|
|
136
140
|
|
|
137
141
|
If the hub has no remote (`platform: null`) or the bridge is disabled, the front-half gate runs
|
|
138
142
|
file-only with no error — the bridge is purely additive.
|
|
@@ -33,7 +33,9 @@ login to an SDLC name + role. It is a single object for the hub itself — the s
|
|
|
33
33
|
|
|
34
34
|
The roster is how a platform identity (a GitHub/GitLab **login**) becomes an SDLC **name + role(s)** in
|
|
35
35
|
the file ledger (`approvals.json` / `comments.json`). Roles are the same three the gate uses:
|
|
36
|
-
`owner | reviewer | domain-owner`.
|
|
36
|
+
`owner | reviewer | domain-owner`. Populate and edit it any time with the **`yad roster`** CLI command
|
|
37
|
+
(`list` / `add` / `grant` / `revoke` / `remove`) — `add` walks the connected repos asking for each
|
|
38
|
+
one's role, and a `domain-owner` grant keeps `repos.json` `domain_owners` in sync.
|
|
37
39
|
|
|
38
40
|
- **`login`** — the platform username whose PR review / approval is being mapped.
|
|
39
41
|
- **`name`** — the SDLC name written into the ledger (the same names used across `approvals.json`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yad-docs-overview
|
|
3
|
-
description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram.
|
|
3
|
+
description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram. Folds the legacy hand-maintained docs/index.html report into the site as report.html (linked from the nav) and deploys via `yad docs deploy --overview`. This is project documentation, not a gated state — it never touches any epic''s state or approvals. Use when the user says "generate the overview site", "build the SDLC overview docs", or after the pipeline (module-help.csv / config.yaml / skill count) changes.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SDLC — Author the Overview Site (project-level, the pipeline as a living map)
|
|
@@ -9,7 +9,7 @@ description: 'Generates the project-level SDLC-overview interactive site — the
|
|
|
9
9
|
reusing the same shell as the per-epic docs (`skills/yad-docs/templates/app/`). Where `yad-docs`
|
|
10
10
|
animates one epic's flows, this animates the **workflow itself**: the front gates, the build half, the
|
|
11
11
|
automation dial, the setup connectors. It is the regenerable successor to the hand-maintained
|
|
12
|
-
`
|
|
12
|
+
overview report, which is folded into this site as `public/report.html`.
|
|
13
13
|
|
|
14
14
|
This is **project documentation, not a gated state** — there is no epic, no `state.json`, no approvals.
|
|
15
15
|
It only reads the pipeline definition and writes a project-level site. When a docs target is connected
|
|
@@ -22,7 +22,7 @@ It only reads the pipeline definition and writes a project-level site. When a do
|
|
|
22
22
|
the generated **source is committed**. The overview build manifest is `docs/sdlc-site/.docs-build.json`.
|
|
23
23
|
- The shell template is `skills/yad-docs/templates/app/` — copied **verbatim**, themed only in the
|
|
24
24
|
`:root` of `index.css`. Generated data satisfies `src/data/types.ts`.
|
|
25
|
-
- Theme: **yadflow's own brand palette** (the `:root` of `docs/
|
|
25
|
+
- Theme: **yadflow's own brand palette** (the `:root` of the legacy report, now `docs/sdlc-site/public/report.html`) — for visual continuity with
|
|
26
26
|
the existing overview, not an epic's design tokens.
|
|
27
27
|
- Speak in the configured `communication_language`; write documents in `document_output_language`.
|
|
28
28
|
|
|
@@ -68,7 +68,7 @@ Map the pipeline onto the same data structures `yad-docs` uses (concrete mapping
|
|
|
68
68
|
Copy the shell from `templates/app/` **verbatim**, generate `src/data/*.ts` deterministically (same
|
|
69
69
|
determinism rules as `yad-docs`: stable-ID sort by skill pipeline order / phase, fixed key order, no
|
|
70
70
|
timestamps in the data files), theme the `:root` of `index.css` from **yadflow's brand palette** — the
|
|
71
|
-
|
|
71
|
+
the legacy report's `:root`: `--accent: #2471a3` and the node colors (`--artifact-*`, `--gate-*`,
|
|
72
72
|
`--earns-*`, `--locked-*`, `--sentinel-*`) — and substitute the Vite base from `.sdlc/docs.json`
|
|
73
73
|
`basePath` (the overview sits at the base root, e.g. `/<repo>/`).
|
|
74
74
|
|
|
@@ -91,11 +91,13 @@ doc-shell upgrade triggers a rebuild). `skillCount` rides along in the manifest
|
|
|
91
91
|
— it is **not** a separate hash input, since `module-help.csv` already moves whenever the skill set does.
|
|
92
92
|
Not per-epic artifacts/repo heads.
|
|
93
93
|
|
|
94
|
-
### Step 5 —
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
### Step 5 — Fold the legacy report into the site
|
|
95
|
+
Relocate the hand-maintained static report into the generated site as `docs/sdlc-site/public/report.html`
|
|
96
|
+
(Vite copies `public/` verbatim into `dist/`, so it publishes alongside the app at `<base>/report.html`),
|
|
97
|
+
and link it from the app nav (a "Full report" link in `TopNavBar`). The interactive overview becomes the
|
|
98
|
+
primary documentation and the legacy report rides along as its detailed companion — no orphaned
|
|
99
|
+
`docs/index.html` at the repo root. This generalizes the standing rule that feature work hand-updates the
|
|
100
|
+
report: the overview site now **regenerates** instead.
|
|
99
101
|
|
|
100
102
|
### Step 6 — Build / deploy (`action`)
|
|
101
103
|
- `action: generate` (default) — generate source + manifest; stop.
|
|
@@ -104,8 +106,8 @@ generated site for this release. This generalizes the standing rule that feature
|
|
|
104
106
|
|
|
105
107
|
### Step 7 — Stop. Report (no gate, no epic)
|
|
106
108
|
Report: the site path (`docs/sdlc-site/`), the data files produced, that the theme is the yadflow brand
|
|
107
|
-
palette, the deploy URL or "build-only", the staleness baseline, and that
|
|
108
|
-
|
|
109
|
+
palette, the deploy URL or "build-only", the staleness baseline, and that the legacy report is folded in
|
|
110
|
+
at `public/report.html` (linked from the nav). Never touches any epic state.
|
|
109
111
|
|
|
110
112
|
## Hard rules
|
|
111
113
|
|
|
@@ -97,6 +97,6 @@ The eight yadflow lenses, each to its relevant phase sections + paths:
|
|
|
97
97
|
## Determinism + theme
|
|
98
98
|
Same discipline as `yad-docs`: stable-sort steps by phase then pipeline order, fixed key order, **no
|
|
99
99
|
timestamps** in `src/data/*.ts` (build time lives only in `.docs-build.json`). Theme the `:root` from
|
|
100
|
-
**yadflow's brand palette** (
|
|
100
|
+
**yadflow's brand palette** (the legacy report's `:root`): `--accent: #2471a3`, and carry the node-class
|
|
101
101
|
colors through to step/path colors — `--artifact-* #b7950b`, `--gate-* #ca6f1e`, `--earns-* #2471a3`,
|
|
102
102
|
`--locked-* #566573`, `--sentinel-* #1e8449` — so the canvas reads like the existing diagram.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
The roster lives in `.sdlc/hub.json` (`roster: [...]`) and is the only thing that turns a platform
|
|
4
4
|
**login** into an SDLC **name + role** for the ledger. Schema and the no-tokens rule are documented once
|
|
5
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).
|
|
6
7
|
|
|
7
8
|
## Entry
|
|
8
9
|
|