yadflow 2.6.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 -11
- package/README.md +30 -5
- package/bin/yad.mjs +36 -1
- package/cli/docs.mjs +298 -0
- package/cli/manifest.mjs +6 -1
- package/cli/roster.mjs +164 -0
- package/cli/setup.mjs +128 -2
- package/package.json +3 -4
- package/skills/sdlc/config.yaml +19 -0
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +4 -0
- package/skills/yad-connect-docs/SKILL.md +132 -0
- package/skills/yad-connect-docs/references/docs-registry.md +74 -0
- package/skills/yad-connect-repos/SKILL.md +4 -0
- package/skills/yad-connect-repos/references/hub-config.md +3 -1
- package/skills/yad-docs/SKILL.md +159 -0
- package/skills/yad-docs/references/data-mapping.md +75 -0
- package/skills/yad-docs/references/theme-map.md +69 -0
- package/skills/yad-docs/templates/app/README.md +31 -0
- package/skills/yad-docs/templates/app/eslint.config.js +23 -0
- package/skills/yad-docs/templates/app/index.html +17 -0
- package/skills/yad-docs/templates/app/package-lock.json +4030 -0
- package/skills/yad-docs/templates/app/package.json +35 -0
- package/skills/yad-docs/templates/app/public/favicon.svg +28 -0
- package/skills/yad-docs/templates/app/public/logo.svg +39 -0
- package/skills/yad-docs/templates/app/public/vite.svg +1 -0
- package/skills/yad-docs/templates/app/src/App.tsx +98 -0
- package/skills/yad-docs/templates/app/src/components/Auth/LoginPage.tsx +101 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/AnimatedMessage.tsx +101 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/ConnectionLine.tsx +90 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +216 -0
- package/skills/yad-docs/templates/app/src/components/Canvas/SystemComponent.tsx +153 -0
- package/skills/yad-docs/templates/app/src/components/Controls/PlaybackBar.tsx +284 -0
- package/skills/yad-docs/templates/app/src/components/Controls/StepDetail.tsx +167 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/HandlerLogicSnippet.tsx +41 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/RequestPayloadPreview.tsx +46 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/RightPanel.tsx +88 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/StatusCard.tsx +76 -0
- package/skills/yad-docs/templates/app/src/components/DetailPanel/TriggerEventCard.tsx +45 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/DocPageShell.tsx +80 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/DocSectionCard.tsx +55 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/DocTableOfContents.tsx +79 -0
- package/skills/yad-docs/templates/app/src/components/DocLayout/RoleCard.tsx +67 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/ApiReferenceSection.tsx +108 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/CancelabilitySection.tsx +73 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/CriticalRunbookSection.tsx +177 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DataMigrationSection.tsx +102 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DbSchemaSection.tsx +98 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DeploymentGuideSection.tsx +104 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/DriverIntegrationSection.tsx +127 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/ExecutiveSummarySection.tsx +69 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/FlowOverviewSection.tsx +73 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/FlowPathsChecklistSection.tsx +96 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/MiddlewareChainSection.tsx +107 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/MonitoringAlertingSection.tsx +106 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/NotificationLocalizationSection.tsx +102 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/PMRoadmapSection.tsx +133 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/PerformanceTestingSection.tsx +91 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/RiderIntegrationSection.tsx +99 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/SecuritySection.tsx +74 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/StatusMachineSection.tsx +90 -0
- package/skills/yad-docs/templates/app/src/components/DocSections/TestPlanSection.tsx +163 -0
- package/skills/yad-docs/templates/app/src/components/Logs/SystemLogsTerminal.tsx +126 -0
- package/skills/yad-docs/templates/app/src/components/Navigation/TopNavBar.tsx +90 -0
- package/skills/yad-docs/templates/app/src/components/Reference/BullMQJobsList.tsx +60 -0
- package/skills/yad-docs/templates/app/src/components/Reference/DecisionTreeView.tsx +49 -0
- package/skills/yad-docs/templates/app/src/components/Reference/DeeplinkActionsChips.tsx +69 -0
- package/skills/yad-docs/templates/app/src/components/Reference/DriverUIStatesTable.tsx +61 -0
- package/skills/yad-docs/templates/app/src/components/Reference/FeatureFlagMatrix.tsx +73 -0
- package/skills/yad-docs/templates/app/src/components/Reference/RiderUIStatesTable.tsx +61 -0
- package/skills/yad-docs/templates/app/src/components/Reference/RulesLegendPanel.tsx +217 -0
- package/skills/yad-docs/templates/app/src/components/Reference/StakeholderToggle.tsx +41 -0
- package/skills/yad-docs/templates/app/src/components/Reference/TroubleshootingSection.tsx +93 -0
- package/skills/yad-docs/templates/app/src/components/Sidebar/PathSelector.tsx +148 -0
- package/skills/yad-docs/templates/app/src/components/Sidebar/SidebarFooter.tsx +40 -0
- package/skills/yad-docs/templates/app/src/components/Sidebar/StepList.tsx +234 -0
- package/skills/yad-docs/templates/app/src/components/shared/Badge.tsx +28 -0
- package/skills/yad-docs/templates/app/src/components/shared/CommandPalette.tsx +213 -0
- package/skills/yad-docs/templates/app/src/components/shared/Icon.tsx +21 -0
- package/skills/yad-docs/templates/app/src/components/shared/Tooltip.tsx +42 -0
- package/skills/yad-docs/templates/app/src/data/components.ts +74 -0
- package/skills/yad-docs/templates/app/src/data/docSections.ts +231 -0
- package/skills/yad-docs/templates/app/src/data/paths.ts +2319 -0
- package/skills/yad-docs/templates/app/src/data/referenceData.ts +392 -0
- package/skills/yad-docs/templates/app/src/data/roles.ts +145 -0
- package/skills/yad-docs/templates/app/src/data/types.ts +79 -0
- package/skills/yad-docs/templates/app/src/hooks/useAnimationQueue.ts +41 -0
- package/skills/yad-docs/templates/app/src/hooks/usePlayback.ts +100 -0
- package/skills/yad-docs/templates/app/src/hooks/useStakeholderFilter.ts +10 -0
- package/skills/yad-docs/templates/app/src/index.css +121 -0
- package/skills/yad-docs/templates/app/src/main.tsx +13 -0
- package/skills/yad-docs/templates/app/src/pages/RoleSelectPage.tsx +34 -0
- package/skills/yad-docs/templates/app/src/pages/StakeholderDocPage.tsx +98 -0
- package/skills/yad-docs/templates/app/src/pages/SubPathDetailPage.tsx +282 -0
- package/skills/yad-docs/templates/app/src/store/useAuthStore.ts +42 -0
- package/skills/yad-docs/templates/app/src/store/useFlowStore.ts +197 -0
- package/skills/yad-docs/templates/app/src/utils/iconMap.ts +46 -0
- package/skills/yad-docs/templates/app/tsconfig.app.json +28 -0
- package/skills/yad-docs/templates/app/tsconfig.json +7 -0
- package/skills/yad-docs/templates/app/tsconfig.node.json +26 -0
- package/skills/yad-docs/templates/app/vite.config.ts +10 -0
- package/skills/yad-docs-overview/SKILL.md +131 -0
- package/skills/yad-docs-overview/references/pipeline-model.md +102 -0
- package/skills/yad-docs-sync/SKILL.md +99 -0
- package/skills/yad-docs-sync/references/staleness.md +81 -0
- package/skills/yad-hub-bridge/references/login-roster.md +1 -0
- package/docs/index.html +0 -1323
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "2.
|
|
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 +
|
|
3
|
+
"version": "2.8.0",
|
|
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",
|
|
7
7
|
"license": "MIT",
|
|
@@ -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"
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -176,6 +176,25 @@ learning:
|
|
|
176
176
|
# tokens in the registry (kb name + sources are plain references, never credentials). Opt-in, never a gate.
|
|
177
177
|
auth: user
|
|
178
178
|
|
|
179
|
+
# Interactive documentation (yad-connect-docs / yad-docs / yad-docs-overview / yad-docs-sync) — the
|
|
180
|
+
# generated React+Vite+Tailwind doc SITES. yad-docs builds a per-epic site (epics/EP-<slug>/docs-site/)
|
|
181
|
+
# from the authored artifacts, themed by the connected design system; yad-docs-overview builds the
|
|
182
|
+
# project SDLC-overview site (docs/sdlc-site/) from the pipeline definition, superseding docs/index.html.
|
|
183
|
+
# The deploy TARGET is a Pages host, auto-detected from hub.platform (github->github-pages,
|
|
184
|
+
# gitlab->gitlab-pages, null->build-only). The yad CLI does the npm build + Pages wiring + staleness;
|
|
185
|
+
# the content generation is the AI step. Docs are an OUTPUT ENRICHMENT — never a gate, never touch state.
|
|
186
|
+
docs:
|
|
187
|
+
registry: "{project-root}/.sdlc/docs.json" # project-wide docs/Pages connection (NOT per-epic)
|
|
188
|
+
targets: [github-pages, gitlab-pages] # supported Pages hosts; auto-detected from hub.platform
|
|
189
|
+
scope: hub # publish from: hub (default) | <repo-name> | dedicated
|
|
190
|
+
degrade: build-only # no Pages host / no gh|glab => build the dist, publish via CI
|
|
191
|
+
login_gate: false # public docs by default (the client-side gate is presentational only)
|
|
192
|
+
manifest: "{project-root}/epics/EP-<slug>/.sdlc/docs-build.json" # per-epic staleness baseline (yad-docs)
|
|
193
|
+
overview: "{project-root}/docs/sdlc-site/" # the project SDLC-overview site (yad-docs-overview)
|
|
194
|
+
# Auth: `yad-connect-docs` records only how to reach the Pages host (platform + base path); it stores NO
|
|
195
|
+
# tokens — publishing runs as the LOCAL user's own gh/glab or the platform CI. Idempotent, refreshable.
|
|
196
|
+
auth: user
|
|
197
|
+
|
|
179
198
|
# Hub platform + front-half review bridge (yad-connect-repos `detect-hub`; yad-review-gate + yad-hub-bridge).
|
|
180
199
|
# The product hub is itself a git repo on a platform. With the bridge enabled, the front-half review/
|
|
181
200
|
# comment/approval cycle runs through a real PR/MR on the hub: a review PR is opened per artifact, reviewers
|
package/skills/sdlc/install.sh
CHANGED
|
@@ -11,7 +11,7 @@ set -euo pipefail
|
|
|
11
11
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
12
12
|
cd "$ROOT"
|
|
13
13
|
|
|
14
|
-
SKILLS=(yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-connect-design yad-connect-testing yad-connect-learning yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
|
|
14
|
+
SKILLS=(yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
|
|
15
15
|
|
|
16
16
|
echo "Installing sdlc module from $ROOT/skills ..."
|
|
17
17
|
|
|
@@ -24,3 +24,7 @@ SDLC Workflow,yad-engineer-review,Engineer Review & Merge,ER,"Build-half Step E:
|
|
|
24
24
|
SDLC Workflow,yad-backfill,Backfill Specs,BF,"Build-half Step G: generate specs for already-built features in an existing repo. Confirm Repomix (npx repomix CLI), pack ONE feature (compress + git logs, secret-scan), feed to AI with a 'describe what exists, do not invent' prompt, write a DRAFT spec marked verified: false. Human approval (reuse yad-review-gate) makes it real. Boundary auto-proposed and human-confirmed. A change is blocked only until the features it touches have approved specs. Never auto-advances.",,{repo: <repo>} {feature: <name + globs>} {action: pack|draft|approve|gate},3-build,,,false,demo-repos/<repo>/specs/backfill/<feature>/,spec.md backfill-check.sh
|
|
25
25
|
SDLC Workflow,yad-run,Run (Automation),RN,"Phase 4 orchestrator: drive a story's back-half loop (spec→tasks→implement→checks) in one code repo, reading each step's automation dial from build-state — on machine_advance it advances on its own, on human_approve it stops for a human. Records every run in the trust log. Realizes Step B (a clean checks pass auto-advances to the engineer review when earned; any FAIL / scope overrun / contract touch HALTS). set-dial earns/reverts a step's automation (machine_advance gated by the trust threshold; front states and engineer-review refused); kill/unkill toggles the system-wide kill switch. Front states and the human merge gate never auto-advance.",,{epic: EP-<slug>} {story: EP-<slug>-S0N} {repo: <repo>} {action: run|set-dial|kill|unkill} {step: <back-step>} {to: human_approve|machine_advance},4-automate,yad-spec,yad-engineer-review,false,epics/EP-<slug>/.sdlc/,build-state/<story-id>.json trust-log.json
|
|
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
|
+
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
|
+
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. 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
|
+
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
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yad-connect-docs
|
|
3
|
+
description: 'Connects a docs/Pages publishing target to the product hub so the interactive-docs steps can build and deploy the generated SPA — not just commit its source. Registers the target into the project-wide .sdlc/docs.json (GitHub Pages / GitLab Pages / build-only), auto-detecting the platform from .sdlc/hub.json and resolving the Vite base path, with local-user auth and no stored tokens. Detects whether gh/glab is present and degrades to build-only when absent. Run at setup or any time the publish target changes. Reusable, idempotent, refreshable. Use when the user says "connect docs", "connect Pages", "refresh the docs connection", or "list the docs connection".'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SDLC — Connect a Docs/Pages Target (make the docs steps publishable)
|
|
7
|
+
|
|
8
|
+
**Goal:** Let the interactive-docs steps (`yad-docs` per epic, `yad-docs-overview` project-wide)
|
|
9
|
+
**build and deploy** the generated React/Vite SPA to a real URL — a GitHub Pages or GitLab Pages site —
|
|
10
|
+
instead of only committing its source. This skill **connects** a publishing target to the product hub
|
|
11
|
+
and records *how* to reach it (the platform, the publish scope, the base path) — never a credential.
|
|
12
|
+
|
|
13
|
+
This is **setup/maintenance**, not a gated front state — it never touches `.sdlc/state.json` or any
|
|
14
|
+
epic's approvals. It only writes the project-wide docs registry. `yad-docs` / `yad-docs-overview`
|
|
15
|
+
consume it: when a target is connected, they theme + generate the site and drive `yad docs deploy`;
|
|
16
|
+
when nothing is connected (`target: "none"`), they still generate and **npm-build** the site but stop
|
|
17
|
+
at a local `dist/` — build-only, no publish, exactly as before.
|
|
18
|
+
|
|
19
|
+
## Conventions
|
|
20
|
+
|
|
21
|
+
- `{project-root}` resolves from the project working directory (the **product hub**).
|
|
22
|
+
- The target is **GitHub-Pages-first but pluggable** — a *publish adapter*, mirroring the GitHub/GitLab
|
|
23
|
+
platform adapter the hub already uses. `github-pages` and `gitlab-pages` are the providers; `none` →
|
|
24
|
+
build-only (deliberate, no error).
|
|
25
|
+
- The platform CLI (`gh` / `glab`) is a **subprocess**, used read/deploy-only via the user's own auth —
|
|
26
|
+
never installed by this skill, never given a token. Absent ⇒ degrade to build-only (`source:
|
|
27
|
+
"unavailable"`).
|
|
28
|
+
- Registry: `{project-root}/.sdlc/docs.json` (project-wide, shared across all epics + the overview —
|
|
29
|
+
NOT per-epic), the sibling of `.sdlc/hub.json`, `.sdlc/repos.json`, and `.sdlc/design.json`.
|
|
30
|
+
- Per-epic / overview build manifests (`docs-build.json`) are written later by `yad-docs` /
|
|
31
|
+
`yad-docs-overview`, not here. This skill describes the *connection*; it does not build.
|
|
32
|
+
- Speak in the configured `communication_language`; write documents in `document_output_language`.
|
|
33
|
+
|
|
34
|
+
## Inputs
|
|
35
|
+
|
|
36
|
+
- `action` — `connect` (default) | `refresh` | `list` | `disconnect`.
|
|
37
|
+
- `target` — `github-pages` | `gitlab-pages` | `none`. Default **auto-detected** from `.sdlc/hub.json`
|
|
38
|
+
`platform` (github → `github-pages`, gitlab → `gitlab-pages`, null/no hub → `none`).
|
|
39
|
+
- `scope` — `hub` (default) | `<repo-name>` | `dedicated`. Where the Pages site is published from (the
|
|
40
|
+
hub repo, one connected code repo, or a dedicated docs repo).
|
|
41
|
+
- `public` — `true` (default) | `false`. Whether the published site is public.
|
|
42
|
+
- `base_path` — optional explicit override of the Vite `base` (otherwise resolved, Step 2).
|
|
43
|
+
|
|
44
|
+
## On Activation
|
|
45
|
+
|
|
46
|
+
### Step 1 — Resolve the target + detect the platform (the publish adapter)
|
|
47
|
+
Determine the `target`. If not given, read `{project-root}/.sdlc/hub.json` `platform` and map it the same
|
|
48
|
+
way the hub bridge maps repos: `github` → `github-pages`, `gitlab` → `gitlab-pages`, `null`/no hub →
|
|
49
|
+
`none` (deliberate build-only). Reject a `target` value outside the three providers (fall back to the
|
|
50
|
+
detected default with a warning, the way `registerRepo` falls back on an unknown platform).
|
|
51
|
+
|
|
52
|
+
Then **probe the platform CLI** in the user's own session — `gh --version` / `gh auth status` for
|
|
53
|
+
GitHub, `glab --version` / `glab auth status` for GitLab. Record `source`:
|
|
54
|
+
- CLI present + authenticated → `source: "gh"` | `"glab"` (deploy can publish via the platform).
|
|
55
|
+
- CLI absent or unauthenticated → `source: "unavailable"` — record it and report that `yad-docs` will
|
|
56
|
+
**build-only** (npm build to a local `dist/`, no publish) until the CLI is available. No error — the
|
|
57
|
+
publish is purely additive, exactly like the `gh`/`glab` review bridge degrading.
|
|
58
|
+
|
|
59
|
+
**Auth is the local user's own** (`gh`/`glab`/git already on this device). The skill **stores no
|
|
60
|
+
tokens**; everything in the registry is a plain reference. Do **not** install a CLI as part of this step.
|
|
61
|
+
|
|
62
|
+
### Step 2 — Decide the publish scope + resolve the base path
|
|
63
|
+
Resolve `scope` → `publishRepo`:
|
|
64
|
+
- `hub` (default) → publish from the hub repo (read its name from `hub.json` `git_url`).
|
|
65
|
+
- `<repo-name>` → publish from that connected code repo (must exist in `.sdlc/repos.json`).
|
|
66
|
+
- `dedicated` → a dedicated docs repo the user names (recorded as `publishRepo`).
|
|
67
|
+
|
|
68
|
+
Resolve `basePath` (the Vite `base`) per the table in `references/docs-registry.md`:
|
|
69
|
+
- **GitHub *project* Pages** (a repo that is not `<user>.github.io`) serve under `/<repo>/` → `basePath =
|
|
70
|
+
"/<repo>/"`. Per-epic sites nest under `/<repo>/epics/EP-<slug>/`; the overview under `/<repo>/`.
|
|
71
|
+
- **GitHub user/org Pages** (`<user>.github.io`) and **GitLab Pages** serve at the domain root → `basePath
|
|
72
|
+
= "/"`.
|
|
73
|
+
- An explicit `base_path` input always wins (recorded verbatim, normalized to a leading + trailing `/`).
|
|
74
|
+
|
|
75
|
+
### Step 3 — Record the connection in the registry (idempotent)
|
|
76
|
+
Upsert into `{project-root}/.sdlc/docs.json` (create the file + parent `.sdlc/` if absent):
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"target": "github-pages",
|
|
81
|
+
"scope": "hub",
|
|
82
|
+
"publishRepo": "<repo or hub name>",
|
|
83
|
+
"basePath": "/<repo>/",
|
|
84
|
+
"public": true,
|
|
85
|
+
"auth": "user",
|
|
86
|
+
"connectedAt": "<YYYY-MM-DD>",
|
|
87
|
+
"lastSyncedAt": "<YYYY-MM-DD>",
|
|
88
|
+
"source": "gh"
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `target: "none"` records a deliberate build-only project: `{ "target": "none", "scope": "hub",
|
|
93
|
+
"publishRepo": null, "basePath": "/", "source": "unavailable", ... }`.
|
|
94
|
+
- `connect` is **idempotent** — re-running it overwrites the single connection in place (a project has
|
|
95
|
+
one docs target at a time; switching targets is just another `connect`).
|
|
96
|
+
|
|
97
|
+
### Step 4 — Report (never auto-advance)
|
|
98
|
+
Report the connected `target`, its `scope`/`publishRepo`, the resolved `basePath`, whether the platform
|
|
99
|
+
CLI is available (or that the docs steps will degrade to **build-only**), and that **`yad-docs` /
|
|
100
|
+
`yad-docs-overview` will now build + deploy here**. Nothing auto-advances; this is setup. **Do not build
|
|
101
|
+
the site here — `yad-docs` builds.**
|
|
102
|
+
|
|
103
|
+
## Other actions
|
|
104
|
+
|
|
105
|
+
- **`refresh`** — re-detect the platform CLI and re-resolve the base path (after the user authenticates a
|
|
106
|
+
session, renames the publish repo, or switches `scope`), updating `lastSyncedAt`. Same machinery as
|
|
107
|
+
`connect`. Re-detection may flip `source` between `gh`/`glab` and `unavailable` — report the change.
|
|
108
|
+
- **`list`** — print the current connection: `target`, `scope`/`publishRepo`, `basePath`, `public`, and a
|
|
109
|
+
**available/unavailable** flag for the platform CLI (best-effort, the user's own session). No target
|
|
110
|
+
connected ⇒ "build-only".
|
|
111
|
+
- **`disconnect`** — remove the registry file (or set `target: "none"`). The platform's own Pages site is
|
|
112
|
+
**never touched** — only the hub's record of it.
|
|
113
|
+
|
|
114
|
+
## Hard rules
|
|
115
|
+
|
|
116
|
+
- **Local-user auth only; store no tokens.** Connect through the user's own `gh`/`glab`/git; never embed a
|
|
117
|
+
PAT or any credential in the registry. Everything recorded is a plain reference.
|
|
118
|
+
- **Degrade gracefully.** No target / no platform CLI → the docs steps **build-only** (local `dist/`) with
|
|
119
|
+
no error. Publishing is additive, never a blocker — the same discipline as the `gh`/`glab` review bridge.
|
|
120
|
+
- **Setup, not a gate.** Never touch `.sdlc/state.json`, approvals, or the contract lock from here.
|
|
121
|
+
- **Idempotent + refreshable.** `connect`/`refresh` are safe to re-run; a project carries one docs
|
|
122
|
+
connection at a time.
|
|
123
|
+
- **Describe the connection; do not build here.** This skill records *how to reach* the target.
|
|
124
|
+
`yad-docs` / `yad-docs-overview` generate, build, and deploy the site.
|
|
125
|
+
|
|
126
|
+
## Reference
|
|
127
|
+
- Registry schema, the base-path resolution table, and the freshness/degrade rules:
|
|
128
|
+
`references/docs-registry.md`.
|
|
129
|
+
- The connect pattern this mirrors (design tool): `../yad-connect-design/SKILL.md`.
|
|
130
|
+
- The connect pattern this mirrors (code repos + hub detection): `../yad-connect-repos/SKILL.md`.
|
|
131
|
+
- The consumers — how `yad-docs` / `yad-docs-overview` build + deploy: `../yad-docs/SKILL.md`,
|
|
132
|
+
`../yad-docs-overview/SKILL.md`.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# `.sdlc/docs.json` — the docs/Pages registry
|
|
2
|
+
|
|
3
|
+
Project-wide, shared across every epic's docs site **and** the project overview site (NOT per-epic).
|
|
4
|
+
The sibling of `.sdlc/hub.json`, `.sdlc/repos.json`, and `.sdlc/design.json`. Written by
|
|
5
|
+
`yad-connect-docs`; read by `yad-docs`, `yad-docs-overview`, `yad-docs-sync`, and the `yad docs` CLI.
|
|
6
|
+
Holds **no credentials** — every field is a plain reference. Auth is always the local user's own
|
|
7
|
+
`gh`/`glab`/git session.
|
|
8
|
+
|
|
9
|
+
## Schema
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"target": "github-pages | gitlab-pages | none",
|
|
14
|
+
"scope": "hub | <repo-name> | dedicated",
|
|
15
|
+
"publishRepo": "<repo or hub name>",
|
|
16
|
+
"basePath": "/<repo>/",
|
|
17
|
+
"public": true,
|
|
18
|
+
"auth": "user",
|
|
19
|
+
"connectedAt": "<YYYY-MM-DD>",
|
|
20
|
+
"lastSyncedAt": "<YYYY-MM-DD>",
|
|
21
|
+
"source": "gh | glab | unavailable"
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Field | Meaning |
|
|
26
|
+
|-------|---------|
|
|
27
|
+
| `target` | The publish adapter. `none` = deliberate build-only (no publish, no error). |
|
|
28
|
+
| `scope` | Where the Pages site publishes from: the `hub` repo, one connected `<repo-name>`, or a `dedicated` docs repo. |
|
|
29
|
+
| `publishRepo` | The concrete repo name resolved from `scope`. `null` when `target: "none"`. |
|
|
30
|
+
| `basePath` | The Vite `base` substituted into each generated site (resolution table below). Normalized to a leading + trailing `/`. |
|
|
31
|
+
| `public` | Whether the published site is public. |
|
|
32
|
+
| `auth` | Always `"user"` — local-user / platform-CLI session. No token is ever stored. |
|
|
33
|
+
| `connectedAt` / `lastSyncedAt` | ISO dates the connection was first written / last re-detected. |
|
|
34
|
+
| `source` | `gh`/`glab` when the platform CLI is present + authenticated (publish works); `unavailable` when absent (build-only). |
|
|
35
|
+
|
|
36
|
+
`target: "none"` records `{ "target": "none", "publishRepo": null, "basePath": "/", "source":
|
|
37
|
+
"unavailable", ... }`.
|
|
38
|
+
|
|
39
|
+
## Platform auto-detection (from `.sdlc/hub.json`)
|
|
40
|
+
|
|
41
|
+
When `target` is not given, map the hub's `platform` the same way `yad-connect-repos` maps a repo host:
|
|
42
|
+
|
|
43
|
+
| `hub.json` `platform` | default `target` |
|
|
44
|
+
|-----------------------|------------------|
|
|
45
|
+
| `github` | `github-pages` |
|
|
46
|
+
| `gitlab` | `gitlab-pages` |
|
|
47
|
+
| `null` / no hub.json | `none` (build-only) |
|
|
48
|
+
|
|
49
|
+
## Base-path resolution table
|
|
50
|
+
|
|
51
|
+
GitHub serves *project* Pages under a `/<repo>/` prefix, so Vite's `base` must match or every asset 404s.
|
|
52
|
+
User/org Pages and GitLab Pages serve at the domain root.
|
|
53
|
+
|
|
54
|
+
| Target + repo kind | `basePath` | Per-epic site URL | Overview site URL |
|
|
55
|
+
|--------------------|------------|-------------------|-------------------|
|
|
56
|
+
| GitHub **project** Pages (repo ≠ `<user>.github.io`) | `/<repo>/` | `/<repo>/epics/EP-<slug>/` | `/<repo>/` |
|
|
57
|
+
| GitHub **user/org** Pages (`<user>.github.io`) | `/` | `/epics/EP-<slug>/` | `/` |
|
|
58
|
+
| GitLab Pages | `/` | `/epics/EP-<slug>/` | `/` |
|
|
59
|
+
| explicit `base_path` input | as given (normalized) | nests `epics/EP-<slug>/` under it | the given base |
|
|
60
|
+
|
|
61
|
+
An explicit `base_path` input always wins. `yad-docs` substitutes `basePath` into the shell's Vite
|
|
62
|
+
config; per-epic sites append `epics/EP-<slug>/` so they nest under the overview without colliding.
|
|
63
|
+
|
|
64
|
+
## Freshness + degrade rules
|
|
65
|
+
|
|
66
|
+
- **Freshness** here is connection-level, not content-level: `lastSyncedAt` reflects the last `refresh`.
|
|
67
|
+
*Site* staleness (artifacts/repos moved, shell upgraded) is tracked separately in each site's
|
|
68
|
+
`docs-build.json` and reconciled by `yad-docs-sync` — not here.
|
|
69
|
+
- **`list`** flags the platform CLI **available/unavailable** by probing `gh auth status` / `glab auth
|
|
70
|
+
status` in the user's own session (best-effort). A flip to `unavailable` means the docs steps
|
|
71
|
+
degrade to build-only until the CLI is back.
|
|
72
|
+
- **Degrade is silent + non-blocking.** No target / no CLI ⇒ `yad-docs` still generates + npm-builds the
|
|
73
|
+
site to a local `dist/`; only the publish step is skipped. The same discipline as the design-tool MCP
|
|
74
|
+
and the `gh`/`glab` review bridge being absent.
|
|
@@ -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`,
|