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/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
# [2.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
### Bug Fixes
|
|
5
|
-
|
|
6
|
-
* allow scoped/breaking commit subjects + titles; parse only the trailer block ([63444c0](https://github.com/abdelrahmannasr/yadflow/commit/63444c08c1e33b4151c7389eb5e87f6ae682aee6))
|
|
7
|
-
* harden pattern-gate CI — pass PR title via env, write body to mktemp ([2415397](https://github.com/abdelrahmannasr/yadflow/commit/2415397f81280f459785b8fa9ca29007b473561b))
|
|
8
|
-
* let `yad ship` derive the PR title from the committed subject ([a5adba3](https://github.com/abdelrahmannasr/yadflow/commit/a5adba3212b236ffc5a2470b9ea50bf97c6c4138))
|
|
1
|
+
# [2.8.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.7.0...v2.8.0) (2026-06-15)
|
|
9
2
|
|
|
10
3
|
|
|
11
4
|
### Features
|
|
12
5
|
|
|
13
|
-
* add `yad
|
|
14
|
-
* add commit-message/pr-title/pr-template pattern gates (code + hub) ([6658837](https://github.com/abdelrahmannasr/yadflow/commit/6658837d7685b826884b665a5e7661fd6ae99828))
|
|
15
|
-
* add yad-commit/yad-open-pr/yad-ship skills; rename Step E to yad-engineer-review ([c566567](https://github.com/abdelrahmannasr/yadflow/commit/c5665679e45f24ea53c682aca3a78eb52c9f984f))
|
|
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))
|
|
16
7
|
|
|
17
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
18
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). |
|
|
@@ -133,9 +134,10 @@ simultaneous advancements can be lost; the next event or scheduled sweep re-sync
|
|
|
133
134
|
### What `setup` walks you through (10 steps)
|
|
134
135
|
|
|
135
136
|
1. **Preflight** — confirm the hub is a git repo (offers `git init`); check `git`/`node`/`npx`.
|
|
136
|
-
2. **Install the module** — copy all
|
|
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) →
|
|
@@ -189,7 +191,7 @@ with a fix-it hint per finding. Failures carry stable, greppable codes, also pri
|
|
|
189
191
|
|
|
190
192
|
Filing a bug? Attach `yad doctor --json` — it contains no secrets (names, paths, and check results only).
|
|
191
193
|
|
|
192
|
-
## Agent skills (all
|
|
194
|
+
## Agent skills (all 29)
|
|
193
195
|
|
|
194
196
|
The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
|
|
195
197
|
AI IDE (e.g. *“run `yad-epic`”*) to actually do the work. State lives in files you can also edit
|
|
@@ -218,6 +220,28 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
|
|
|
218
220
|
tokens), detecting the **DeepTutor CLI on PATH** (a subprocess like Repomix — DeepTutor ships no MCP)
|
|
219
221
|
and degrading to **harness-native** tutoring when absent. Idempotent and refreshable; one connection
|
|
220
222
|
per project.
|
|
223
|
+
- **`yad-connect-docs`** — Connects a docs/Pages target (GitHub Pages / GitLab Pages, auto-detected from
|
|
224
|
+
`hub.json`) so the generated documentation sites can deploy. Records the target + scope + base path in
|
|
225
|
+
`.sdlc/docs.json` (local-user auth, no stored tokens), degrading to **build-only** when no Pages host /
|
|
226
|
+
CLI is present. Idempotent and refreshable; one connection per project.
|
|
227
|
+
|
|
228
|
+
### Living documentation (generated, themed, auto-kept-fresh)
|
|
229
|
+
|
|
230
|
+
- **`yad-docs`** — Generates an **interactive documentation site** for an epic (a React + Vite + Tailwind
|
|
231
|
+
SPA: an animated front-stage flow canvas + role-based stakeholder doc pages) from the authored
|
|
232
|
+
artifacts — `epic.md`, `architecture.md`, the locked `contract.md`, `ui-design.md`, the stories — into
|
|
233
|
+
`epics/EP-<slug>/docs-site/`, themed by the **connected design system** (`DESIGN.md` / `design.json`
|
|
234
|
+
tokens → the site's CSS). The content lives in generated `src/data/*.ts`; the shell is a vendored
|
|
235
|
+
template. An **output enrichment, never a gate** — it never touches epic state, approvals, or the
|
|
236
|
+
contract lock. `generate` / `refresh` / `deploy`.
|
|
237
|
+
- **`yad-docs-overview`** — Generates the project **SDLC-overview site** (`docs/sdlc-site/`) — every
|
|
238
|
+
stage from setup → ship as flow paths / system components / stakeholder roles, reusing the same shell —
|
|
239
|
+
superseding the hand-maintained `docs/index.html` (folded into the site as `public/report.html`, linked
|
|
240
|
+
from the nav).
|
|
241
|
+
- **`yad-docs-sync`** — Keeps the sites fresh: detects staleness (a content hash of the authored
|
|
242
|
+
artifacts + the connected repos' HEAD shas vs each site's build manifest), regenerates + redeploys, and
|
|
243
|
+
can wire a CI job that rebuilds on push. Generalizes the rule that feature work must hand-update the
|
|
244
|
+
docs — the overview now regenerates whenever the skill set / pipeline changes.
|
|
221
245
|
|
|
222
246
|
### The learning layer (cross-cutting — any member, any stage)
|
|
223
247
|
|
|
@@ -366,9 +390,10 @@ detailed sections below expand every phase. Invoke a skill by name in your agent
|
|
|
366
390
|
`testing.json`, lets `yad-test-cases` implement automation), `yad-connect-learning action: connect`
|
|
367
391
|
(DeepTutor-first → `learning.json`, powers the cross-cutting learning layer).
|
|
368
392
|
7. **(Optional) Put the hub on a platform** so the front-half review runs through real PRs:
|
|
369
|
-
`yad-connect-repos action: detect-hub`, then `
|
|
370
|
-
name +
|
|
371
|
-
|
|
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.
|
|
372
397
|
8. **Conventions:** commits and PR/MR titles follow Conventional Commits (lowercase after the type), the
|
|
373
398
|
human author owns each commit with an optional per-commit `Co-Authored-By` AI trailer — see
|
|
374
399
|
[`CONTRIBUTING.md`](CONTRIBUTING.md).
|
package/bin/yad.mjs
CHANGED
|
@@ -10,6 +10,8 @@ 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';
|
|
14
|
+
import { runDocs } from '../cli/docs.mjs';
|
|
13
15
|
import { runDoctor } from '../cli/doctor.mjs';
|
|
14
16
|
|
|
15
17
|
const HELP = `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
|
|
@@ -23,6 +25,14 @@ ${c.bold('Setup & maintenance')}
|
|
|
23
25
|
yad doctor [--json] Environment + state health: tools/auth, config files,
|
|
24
26
|
repo paths, epic ledgers (exit 1 on any failure)
|
|
25
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
|
+
|
|
26
36
|
${c.bold('Review gate (front half)')}
|
|
27
37
|
yad gate open <epic> <artifact> Open the review PR/MR; mark the step in_review
|
|
28
38
|
yad gate sync <epic> [artifact] Pull PR state -> ledger; advance on approved+resolved+merged
|
|
@@ -39,6 +49,12 @@ ${c.bold('Build helpers')}
|
|
|
39
49
|
yad repo list Show connected repos (fresh / stale)
|
|
40
50
|
yad repo refresh [name] Re-pack a stale repo (a human decision)
|
|
41
51
|
|
|
52
|
+
${c.bold('Interactive docs (generated sites)')}
|
|
53
|
+
yad docs list Show the docs target + per-site freshness
|
|
54
|
+
yad docs build [--epic <id>|--overview] npm-build a generated doc site
|
|
55
|
+
yad docs deploy [--epic <id>|--overview] Build + report the Pages deploy
|
|
56
|
+
yad docs sync [--check|--refresh|--wire] Staleness sweep; --wire installs the Pages CI
|
|
57
|
+
|
|
42
58
|
${c.bold('Options')}
|
|
43
59
|
--dir <path> Target project root (default: cwd)
|
|
44
60
|
--type <t> commit: feat|fix|docs|refactor|test|perf|build|ci|chore|revert
|
|
@@ -48,6 +64,9 @@ ${c.bold('Options')}
|
|
|
48
64
|
--contract-change commit/open-pr: mark the contract surface touched
|
|
49
65
|
--risk <level> open-pr: low|medium|high (default low)
|
|
50
66
|
--repo <name> open-pr: target a registered repo by name
|
|
67
|
+
--epic <id> docs: target one epic's site (EP-<slug>)
|
|
68
|
+
--overview docs: target the project SDLC-overview site
|
|
69
|
+
--check/--refresh/--wire docs sync: report stale / rebuild / install Pages CI
|
|
51
70
|
--dry-run commit: print the message, do not commit
|
|
52
71
|
--force commit: bypass the atomic-file guard / re-copy unchanged files
|
|
53
72
|
--branch <head> gate ci: the review PR/MR head branch (review/EP-<slug>/<artifact>)
|
|
@@ -56,7 +75,7 @@ ${c.bold('Options')}
|
|
|
56
75
|
-h, --help Show this help
|
|
57
76
|
-v, --version Print version`;
|
|
58
77
|
|
|
59
|
-
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr']);
|
|
78
|
+
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--name', '--email', '--roles']);
|
|
60
79
|
|
|
61
80
|
function parseArgs(argv) {
|
|
62
81
|
const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
|
|
@@ -66,6 +85,10 @@ function parseArgs(argv) {
|
|
|
66
85
|
else if (a === '--force') o.force = true;
|
|
67
86
|
else if (a === '--contract-change') o.contractChange = true;
|
|
68
87
|
else if (a === '--no-push') o.noPush = true;
|
|
88
|
+
else if (a === '--overview') o.overview = true;
|
|
89
|
+
else if (a === '--check') o.check = true;
|
|
90
|
+
else if (a === '--refresh') o.refresh = true;
|
|
91
|
+
else if (a === '--wire') o.wire = true;
|
|
69
92
|
else if (a === '--dry-run') o.dryRun = true;
|
|
70
93
|
else if (a === '--json') o.json = true;
|
|
71
94
|
else if (a === '-h' || a === '--help') o.help = true;
|
|
@@ -133,6 +156,18 @@ async function main() {
|
|
|
133
156
|
await runRepo(o.dir, { action: action || 'list', name, today });
|
|
134
157
|
break;
|
|
135
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
|
+
}
|
|
164
|
+
case 'docs': {
|
|
165
|
+
const [, action] = o._;
|
|
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; }
|
|
167
|
+
const sync = o.wire ? 'wire' : o.refresh ? 'refresh' : 'check';
|
|
168
|
+
await runDocs(o.dir, { action: action || 'list', epic: o.epic, overview: o.overview, sync, today });
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
136
171
|
default:
|
|
137
172
|
log(c.red(`unknown command: ${cmd}`));
|
|
138
173
|
log(HELP);
|
package/cli/docs.mjs
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// `yad docs list|build|deploy|sync` — the build/deploy/staleness mechanics for the interactive
|
|
2
|
+
// documentation sites generated by the yad-docs / yad-docs-overview skills. The CONTENT generation
|
|
3
|
+
// (reading epic/architecture/contract/ui/stories → writing src/data/*.ts and theming index.css) is
|
|
4
|
+
// the AI step inside those skills; this module only does the mechanical parts: npm build (a
|
|
5
|
+
// subprocess, like yad-spec shelling npx repomix), the platform Pages wiring (reusing platform.mjs),
|
|
6
|
+
// and the manifest-hash staleness check (reusing the head-sha idea from repo.mjs). It NEVER touches
|
|
7
|
+
// epic state, approvals, or the contract lock — docs are an output enrichment, not a gate.
|
|
8
|
+
//
|
|
9
|
+
// Pure mapping fns (deployTargetFromHub / siteBasePath / docsArtifactHash / docsStale / pagesWorkflow)
|
|
10
|
+
// are exported for unit tests; the side-effecting runDocs orchestrates them.
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { c, log, ok, info, warn, hand, fail, readJSON, run, has, exists } from './lib.mjs';
|
|
15
|
+
import { PROJECT_FILES, VERSION } from './manifest.mjs';
|
|
16
|
+
import { detectPlatform, platformReady } from './platform.mjs';
|
|
17
|
+
import { gitHead } from './setup.mjs';
|
|
18
|
+
import { contractSurfaceHash } from './epic-state.mjs';
|
|
19
|
+
|
|
20
|
+
// ---- registry + site locations ------------------------------------------------------------------
|
|
21
|
+
function loadDocs(root) {
|
|
22
|
+
const regPath = path.join(root, PROJECT_FILES.docsConfig);
|
|
23
|
+
return { regPath, docs: readJSON(regPath, null) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Per-epic site lives beside the epic artifacts; the overview is project-level under docs/.
|
|
27
|
+
export function siteDir(root, { epic, overview } = {}) {
|
|
28
|
+
if (overview) return path.join(root, 'docs/sdlc-site');
|
|
29
|
+
return path.join(root, 'epics', epic, 'docs-site');
|
|
30
|
+
}
|
|
31
|
+
export function manifestPath(root, { epic, overview } = {}) {
|
|
32
|
+
if (overview) return path.join(siteDir(root, { overview }), '.docs-build.json');
|
|
33
|
+
return path.join(root, 'epics', epic, '.sdlc/docs-build.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- pure: platform/target + base path ----------------------------------------------------------
|
|
37
|
+
// hub.json platform -> the default Pages target (github-pages | gitlab-pages | none/build-only).
|
|
38
|
+
export function deployTargetFromHub(hub = {}) {
|
|
39
|
+
const platform = hub?.platform || detectPlatform(hub?.git_url || '');
|
|
40
|
+
if (platform === 'github') return 'github-pages';
|
|
41
|
+
if (platform === 'gitlab') return 'gitlab-pages';
|
|
42
|
+
return 'none';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The Vite `base` for one site: join the project base path (from docs.json) with the per-site
|
|
46
|
+
// subpath. Per-epic sites nest under epics/<id>/; the overview is the Pages root. Always a
|
|
47
|
+
// leading+trailing slash so it works as a Vite base and a router basename.
|
|
48
|
+
export function siteBasePath(docs = {}, { epic, overview } = {}) {
|
|
49
|
+
const root = (docs.basePath || '/').replace(/\/+$/, '') || '';
|
|
50
|
+
const sub = overview ? '' : epic ? `/epics/${epic}` : '';
|
|
51
|
+
const joined = `${root}${sub}`.replace(/\/+/g, '/');
|
|
52
|
+
return joined ? `${joined.replace(/\/$/, '')}/` : '/';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- pure: staleness ----------------------------------------------------------------------------
|
|
56
|
+
// The artifacts whose content a per-epic site is generated from (any that exist contribute).
|
|
57
|
+
// contract.md is deliberately EXCLUDED here: it is folded in via its CONTRACT-SURFACE block hash
|
|
58
|
+
// (docsArtifactHash's `extra`), so docs-staleness tracks the same locked surface as the contract
|
|
59
|
+
// lock — non-surface prose edits to contract.md don't needlessly mark the docs stale.
|
|
60
|
+
export function docsArtifactFiles(root, epic) {
|
|
61
|
+
const epicRoot = path.join(root, 'epics', epic);
|
|
62
|
+
const flat = ['epic.md', 'architecture.md', 'ui-design.md', 'DESIGN.md', 'test-cases.md']
|
|
63
|
+
.map((f) => path.join(epicRoot, f));
|
|
64
|
+
const storiesDir = path.join(epicRoot, 'stories');
|
|
65
|
+
const stories = exists(storiesDir)
|
|
66
|
+
? fs.readdirSync(storiesDir).filter((f) => f.endsWith('.md')).sort().map((f) => path.join(storiesDir, f))
|
|
67
|
+
: [];
|
|
68
|
+
return [...flat, ...stories].filter(exists).sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// sha256 over the (sorted) artifact files' bytes plus an optional `extra` string (the contract
|
|
72
|
+
// surface hash) — the content baseline a site was built from. Deterministic: the file list is
|
|
73
|
+
// sorted and each file is length-prefixed so concatenation is unambiguous (a + bc never collides
|
|
74
|
+
// with ab + c).
|
|
75
|
+
export function docsArtifactHash(files = [], extra = '') {
|
|
76
|
+
const h = createHash('sha256');
|
|
77
|
+
for (const f of [...files].sort()) {
|
|
78
|
+
const buf = fs.readFileSync(f);
|
|
79
|
+
h.update(`${path.basename(f)}:${buf.length}\n`);
|
|
80
|
+
h.update(buf);
|
|
81
|
+
}
|
|
82
|
+
if (extra) h.update(`\ncontract-surface:${extra}`);
|
|
83
|
+
return 'sha256:' + h.digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Current HEAD sha per repo the epic touches (mirrors repo.mjs head-sha staleness).
|
|
87
|
+
export function repoHeadsFor(root, repos = [], registry = { repos: [] }) {
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const name of repos) {
|
|
90
|
+
const entry = (registry.repos || []).find((r) => r.name === name);
|
|
91
|
+
if (!entry) continue;
|
|
92
|
+
out[name] = gitHead(path.resolve(root, entry.path)) || null;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Compare a build manifest to the current world; list the concrete reasons it is stale.
|
|
98
|
+
export function docsStale(manifest, { artifactHash, repoHeads = {}, templateVersion } = {}) {
|
|
99
|
+
const reasons = [];
|
|
100
|
+
if (!manifest) return { stale: true, reasons: ['never built'] };
|
|
101
|
+
if (artifactHash && manifest.artifactHash && artifactHash !== manifest.artifactHash) {
|
|
102
|
+
reasons.push('authored artifacts changed');
|
|
103
|
+
}
|
|
104
|
+
for (const [repo, head] of Object.entries(repoHeads)) {
|
|
105
|
+
const was = (manifest.repoHeads || {})[repo];
|
|
106
|
+
if (head && was && head !== was) reasons.push(`repo ${repo} HEAD advanced`);
|
|
107
|
+
}
|
|
108
|
+
if (templateVersion && manifest.templateVersion && templateVersion !== manifest.templateVersion) {
|
|
109
|
+
reasons.push(`doc shell upgraded (${manifest.templateVersion} → ${templateVersion})`);
|
|
110
|
+
}
|
|
111
|
+
return { stale: reasons.length > 0, reasons };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- pure: the Pages CI workflow (committed by `yad docs sync --wire`) ---------------------------
|
|
115
|
+
// GitHub Actions deploy-pages, or a GitLab `pages` job. Both assemble a single `public/` tree: the
|
|
116
|
+
// overview at the root and every per-epic site under `epics/<id>/` — matching siteBasePath's nesting
|
|
117
|
+
// (overview at `<base>/`, epics at `<base>/epics/EP-<slug>/`). A concurrency group prevents the
|
|
118
|
+
// deploy from retriggering. The shared shell script keeps the two platforms byte-for-byte aligned.
|
|
119
|
+
const BUILD_PUBLIC = [
|
|
120
|
+
'mkdir -p public',
|
|
121
|
+
'if [ -d docs/sdlc-site ]; then (cd docs/sdlc-site && npm ci && npm run build) && cp -r docs/sdlc-site/dist/. public/; fi',
|
|
122
|
+
'for d in epics/*/docs-site; do [ -d "$d" ] || continue; id=$(basename "$(dirname "$d")"); (cd "$d" && npm ci && npm run build) && mkdir -p "public/epics/$id" && cp -r "$d/dist/." "public/epics/$id/"; done',
|
|
123
|
+
];
|
|
124
|
+
export function pagesWorkflow(platform) {
|
|
125
|
+
if (platform === 'gitlab') {
|
|
126
|
+
return `# yad-managed — built by \`yad docs sync --wire\`. include it from .gitlab-ci.yml:
|
|
127
|
+
# include: { local: .gitlab/ci/yad-docs.yml }
|
|
128
|
+
# Edit the skill, not this file.
|
|
129
|
+
pages:
|
|
130
|
+
stage: deploy
|
|
131
|
+
image: node:20
|
|
132
|
+
rules:
|
|
133
|
+
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
134
|
+
script:
|
|
135
|
+
${BUILD_PUBLIC.map((l) => ` - '${l.replace(/'/g, "'\\''")}'`).join('\n')}
|
|
136
|
+
artifacts:
|
|
137
|
+
paths: [public]
|
|
138
|
+
resource_group: pages
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
return `# yad-managed — built by \`yad docs sync --wire\`. Edit the skill, not this file.
|
|
142
|
+
name: yad-docs
|
|
143
|
+
on:
|
|
144
|
+
push:
|
|
145
|
+
branches: [main, master]
|
|
146
|
+
workflow_dispatch:
|
|
147
|
+
permissions:
|
|
148
|
+
contents: read
|
|
149
|
+
pages: write
|
|
150
|
+
id-token: write
|
|
151
|
+
concurrency:
|
|
152
|
+
group: yad-docs-pages
|
|
153
|
+
cancel-in-progress: true
|
|
154
|
+
jobs:
|
|
155
|
+
build-deploy:
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
environment:
|
|
158
|
+
name: github-pages
|
|
159
|
+
url: \${{ steps.deployment.outputs.page_url }}
|
|
160
|
+
steps:
|
|
161
|
+
- uses: actions/checkout@v4
|
|
162
|
+
- uses: actions/setup-node@v4
|
|
163
|
+
with:
|
|
164
|
+
node-version: 20
|
|
165
|
+
- name: Build the overview + per-epic sites into ./public
|
|
166
|
+
run: |
|
|
167
|
+
${BUILD_PUBLIC.map((l) => ` ${l}`).join('\n')}
|
|
168
|
+
- uses: actions/configure-pages@v5
|
|
169
|
+
- uses: actions/upload-pages-artifact@v3
|
|
170
|
+
with:
|
|
171
|
+
path: public
|
|
172
|
+
- id: deployment
|
|
173
|
+
uses: actions/deploy-pages@v4
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
export function pagesWorkflowPath(platform) {
|
|
177
|
+
return platform === 'gitlab' ? '.gitlab/ci/yad-docs.yml' : '.github/workflows/yad-docs.yml';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---- build (subprocess) -------------------------------------------------------------------------
|
|
181
|
+
function buildSite(dir) {
|
|
182
|
+
if (!exists(dir)) { warn(`no generated site at ${path.relative(process.cwd(), dir)} — run the yad-docs skill first`); return { ok: false, missing: true }; }
|
|
183
|
+
if (!has('npm')) { warn('npm not on PATH — cannot build; the CI workflow will build on push'); return { ok: false, noNpm: true }; }
|
|
184
|
+
const install = exists(path.join(dir, 'package-lock.json')) ? ['ci'] : ['install'];
|
|
185
|
+
log(` ${c.dim('$')} npm ${install[0]} ${c.dim(`(${path.relative(process.cwd(), dir)})`)}`);
|
|
186
|
+
const i = run('npm', install, { cwd: dir, stdio: 'inherit' });
|
|
187
|
+
if (!i.ok) { fail('npm install failed'); return { ok: false }; }
|
|
188
|
+
const b = run('npm', ['run', 'build'], { cwd: dir, stdio: 'inherit' });
|
|
189
|
+
if (!b.ok) { fail('npm run build failed'); return { ok: false }; }
|
|
190
|
+
return { ok: true, dist: path.join(dir, 'dist') };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---- orchestration ------------------------------------------------------------------------------
|
|
194
|
+
export async function runDocs(root, { action = 'list', epic, overview, sync } = {}) {
|
|
195
|
+
const { docs } = loadDocs(root);
|
|
196
|
+
const targets = overview ? [{ overview: true }] : epic ? [{ epic }] : enumerateSites(root);
|
|
197
|
+
|
|
198
|
+
if (action === 'list') {
|
|
199
|
+
if (!docs) { warn('no docs target connected (.sdlc/docs.json) — run yad-connect-docs'); }
|
|
200
|
+
else {
|
|
201
|
+
log(c.bold('\ndocs target'));
|
|
202
|
+
info(`target ${c.cyan(docs.target)} scope ${docs.scope} base ${docs.basePath} ${docs.source === 'unavailable' ? c.yellow('(build-only)') : ''}`);
|
|
203
|
+
}
|
|
204
|
+
log(c.bold('\ngenerated sites'));
|
|
205
|
+
if (!targets.length) { info('none generated yet'); return { sites: 0 }; }
|
|
206
|
+
for (const t of targets) reportFreshness(root, t);
|
|
207
|
+
return { sites: targets.length };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (action === 'build' || action === 'deploy') {
|
|
211
|
+
let built = 0;
|
|
212
|
+
for (const t of targets) {
|
|
213
|
+
const r = buildSite(siteDir(root, t));
|
|
214
|
+
if (r.ok) { built++; ok(`built ${label(t)} ${c.dim('→ ' + path.relative(root, r.dist))}`); }
|
|
215
|
+
}
|
|
216
|
+
if (action === 'deploy') {
|
|
217
|
+
const platform = docs ? (docs.target === 'gitlab-pages' ? 'gitlab' : docs.target === 'github-pages' ? 'github' : null) : null;
|
|
218
|
+
if (!platform || !platformReady(platform)) {
|
|
219
|
+
hand('no Pages platform/CLI — built locally only; commit + push so the CI workflow can publish (yad docs sync --wire)');
|
|
220
|
+
} else {
|
|
221
|
+
ok(`deploy via the ${platform} Pages workflow on push (yad docs sync --wire installs it)`);
|
|
222
|
+
if (docs?.basePath) info(`will publish under ${docs.basePath}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { built };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (action === 'sync') {
|
|
229
|
+
if (sync === 'wire') return wirePages(root, docs);
|
|
230
|
+
// check (default) + refresh both compute staleness; refresh additionally rebuilds.
|
|
231
|
+
const registry = readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] });
|
|
232
|
+
let stale = 0;
|
|
233
|
+
for (const t of targets) {
|
|
234
|
+
const s = freshness(root, t, registry);
|
|
235
|
+
if (s.stale) {
|
|
236
|
+
stale++;
|
|
237
|
+
warn(`${label(t)} — ${c.yellow('stale')}: ${s.reasons.join('; ')}`);
|
|
238
|
+
if (sync === 'refresh') buildSite(siteDir(root, t));
|
|
239
|
+
} else ok(`${label(t)} ${c.dim('— fresh')}`);
|
|
240
|
+
}
|
|
241
|
+
if (stale && sync !== 'refresh') hand('regenerate content with the yad-docs / yad-docs-overview skill (the AI step), then `yad docs deploy`');
|
|
242
|
+
return { stale };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fail(`unknown docs action: ${action} (list | build | deploy | sync)`);
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- helpers ------------------------------------------------------------------------------------
|
|
251
|
+
function enumerateSites(root) {
|
|
252
|
+
const out = [];
|
|
253
|
+
if (exists(siteDir(root, { overview: true }))) out.push({ overview: true });
|
|
254
|
+
const epicsDir = path.join(root, 'epics');
|
|
255
|
+
if (exists(epicsDir)) {
|
|
256
|
+
for (const e of fs.readdirSync(epicsDir).sort()) {
|
|
257
|
+
if (exists(path.join(epicsDir, e, 'docs-site'))) out.push({ epic: e });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function label(t) { return t.overview ? 'overview (docs/sdlc-site)' : `epic ${t.epic}`; }
|
|
263
|
+
|
|
264
|
+
function freshness(root, t, registry) {
|
|
265
|
+
const manifest = readJSON(manifestPath(root, t), null);
|
|
266
|
+
if (t.overview) {
|
|
267
|
+
// The overview is generated from the project pipeline definition, not an epic's artifacts.
|
|
268
|
+
const files = ['skills/sdlc/config.yaml', 'skills/sdlc/module-help.csv', 'docs/diagrams/sdlc-overview.mmd']
|
|
269
|
+
.map((f) => path.join(root, f)).filter(exists);
|
|
270
|
+
return docsStale(manifest, { artifactHash: docsArtifactHash(files), templateVersion: VERSION });
|
|
271
|
+
}
|
|
272
|
+
const epicMeta = readJSON(path.join(root, 'epics', t.epic, '.sdlc/state.json'), {});
|
|
273
|
+
const repos = epicMeta.repos || [];
|
|
274
|
+
const surface = contractSurfaceHash(path.join(root, 'epics', t.epic)); // null when no locked surface
|
|
275
|
+
return docsStale(manifest, {
|
|
276
|
+
artifactHash: docsArtifactHash(docsArtifactFiles(root, t.epic), surface || ''),
|
|
277
|
+
repoHeads: repoHeadsFor(root, repos, registry),
|
|
278
|
+
templateVersion: VERSION,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function reportFreshness(root, t) {
|
|
282
|
+
const registry = readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] });
|
|
283
|
+
const s = freshness(root, t, registry);
|
|
284
|
+
if (s.stale) warn(`${label(t)} — ${c.yellow('stale')}: ${s.reasons.join('; ')}`);
|
|
285
|
+
else ok(`${label(t)} ${c.dim('— fresh')}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function wirePages(root, docs) {
|
|
289
|
+
const platform = docs?.target === 'gitlab-pages' ? 'gitlab' : 'github';
|
|
290
|
+
const rel = pagesWorkflowPath(platform);
|
|
291
|
+
const dest = path.join(root, rel);
|
|
292
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
293
|
+
fs.writeFileSync(dest, pagesWorkflow(platform));
|
|
294
|
+
ok(`wired ${rel} ${c.dim(`(${platform} Pages)`)}`);
|
|
295
|
+
if (platform === 'gitlab') hand(`include it from .gitlab-ci.yml: include: { local: ${rel} }`);
|
|
296
|
+
else hand('commit it; set the repo Pages source to "GitHub Actions" so it publishes on push');
|
|
297
|
+
return { wired: rel };
|
|
298
|
+
}
|
package/cli/manifest.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync } from 'node:fs';
|
|
|
10
10
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
11
11
|
export const VERSION = version;
|
|
12
12
|
|
|
13
|
-
// The
|
|
13
|
+
// The 29 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
|
|
14
14
|
export const SKILLS = [
|
|
15
15
|
'yad-analysis',
|
|
16
16
|
'yad-epic',
|
|
@@ -22,6 +22,10 @@ export const SKILLS = [
|
|
|
22
22
|
'yad-connect-design',
|
|
23
23
|
'yad-connect-testing',
|
|
24
24
|
'yad-connect-learning',
|
|
25
|
+
'yad-connect-docs',
|
|
26
|
+
'yad-docs',
|
|
27
|
+
'yad-docs-overview',
|
|
28
|
+
'yad-docs-sync',
|
|
25
29
|
'yad-learn',
|
|
26
30
|
'yad-spec',
|
|
27
31
|
'yad-implement',
|
|
@@ -121,6 +125,7 @@ export const PROJECT_FILES = {
|
|
|
121
125
|
designConfig: '.sdlc/design.json',
|
|
122
126
|
testingConfig: '.sdlc/testing.json',
|
|
123
127
|
learningConfig: '.sdlc/learning.json',
|
|
128
|
+
docsConfig: '.sdlc/docs.json',
|
|
124
129
|
version: '.sdlc/cli-version.json',
|
|
125
130
|
};
|
|
126
131
|
|