| 1 | # Plan 025: Guard against `skills/` drifting from `docs/` |
| 2 | |
| 3 | > **Executor instructions**: Follow this plan step by step. Run every |
| 4 | > verification command and confirm the expected result. If anything in "STOP |
| 5 | > conditions" occurs, stop and report. When done, update the status row in |
| 6 | > `plans/README.md`. |
| 7 | > |
| 8 | > **Drift check (run first)**: `git diff --stat c63cb120..HEAD -- skills/ scripts/ .github/workflows/` |
| 9 | > On a mismatch with the excerpts below, treat it as a STOP condition. |
| 10 | |
| 11 | ## Status |
| 12 | |
| 13 | - **Priority**: P3 |
| 14 | - **Effort**: S-M |
| 15 | - **Risk**: LOW |
| 16 | - **Depends on**: none |
| 17 | - **Category**: docs / dx |
| 18 | - **Planned at**: commit `c63cb120`, 2026-07-10 |
| 19 | |
| 20 | ## Why this matters |
| 21 | |
| 22 | `skills/slidev/**` is an agent-facing reference **generated from `docs/`** and is |
| 23 | **shipped to every user** inside `@slidev/cli` (`scripts/publish.mjs:4` copies |
| 24 | `skills` into `packages/slidev/skills`). Its provenance is recorded in |
| 25 | `skills/GENERATION.md` as SHA `9d300814` / v52.11.3 — but the repo is v52.16.0 |
| 26 | with dozens of `docs/` commits since, and the only sync mechanism is a **manual** |
| 27 | human checklist. So stale guidance ships to users' agents invisibly. This plan |
| 28 | adds an automated **drift guard**: CI fails when `docs/` has advanced past the |
| 29 | SHA the skills were generated from, forcing a re-sync + provenance bump. (It does |
| 30 | NOT auto-regenerate — that generator is a separate, larger effort.) |
| 31 | |
| 32 | ## Current state |
| 33 | |
| 34 | - `skills/GENERATION.md` records provenance in prose: |
| 35 | - `**Short SHA**: \`9d300814\`` (`:10`) |
| 36 | - a version-history table (`:194-196`) |
| 37 | - `Current SHA: 9d300814` (final line) |
| 38 | There is no machine-readable marker and no CI check. |
| 39 | - `scripts/publish.mjs:4`: `await fs.copy('skills', 'packages/slidev/skills', { overwrite: true })`. |
| 40 | - `scripts/` already contains Node/zx scripts (e.g. `publish.mjs`, |
| 41 | `update-versions.mjs`), so adding a script is idiomatic. |
| 42 | - CI workflows live in `.github/workflows/` (see `test.yml`). |
| 43 | |
| 44 | ## Commands you will need |
| 45 | |
| 46 | | Purpose | Command | Expected | |
| 47 | |---------|---------|----------| |
| 48 | | Run the new check | `node scripts/check-skills-drift.mjs` | exit 0 when in sync; non-zero + message when drifted | |
| 49 | | YAML sanity | `pnpm dlx js-yaml .github/workflows/<file>.yml` | parses | |
| 50 | |
| 51 | (No build/deps needed; the script uses `git` + Node fs.) |
| 52 | |
| 53 | ## Scope |
| 54 | |
| 55 | **In scope**: |
| 56 | - `skills/GENERATION.md` (add a machine-readable provenance marker) |
| 57 | - `scripts/check-skills-drift.mjs` (create) |
| 58 | - `.github/workflows/` (add a job/step that runs the check) |
| 59 | |
| 60 | **Out of scope**: |
| 61 | - Writing the actual skills **generator** (that's a separate direction item, D2). |
| 62 | - Editing `skills/slidev/**` content (do not hand-edit generated files). |
| 63 | - Changing `publish.mjs`. |
| 64 | |
| 65 | ## Git workflow |
| 66 | |
| 67 | - Branch: `ci/skills-drift-guard`. |
| 68 | - Conventional commit: `ci: fail when skills drift from docs`. |
| 69 | - Do NOT push/PR unless instructed. |
| 70 | |
| 71 | ## Steps |
| 72 | |
| 73 | ### Step 1: Add a machine-readable provenance marker |
| 74 | |
| 75 | Add a single unambiguous line to `skills/GENERATION.md` (near the top) that the |
| 76 | script parses, seeded with the currently-recorded SHA: |
| 77 | ```md |
| 78 | <!-- skills-generated-from: 9d300814 --> |
| 79 | ``` |
| 80 | Keep the existing human-readable fields too; this marker is the source of truth |
| 81 | for the check. |
| 82 | |
| 83 | ### Step 2: Write `scripts/check-skills-drift.mjs` |
| 84 | |
| 85 | The script: |
| 86 | 1. Reads the marker SHA from `skills/GENERATION.md` (regex on |
| 87 | `skills-generated-from:\s*([0-9a-f]+)`); error if absent. |
| 88 | 2. Runs `git rev-list --count <sha>..HEAD -- docs/` (via `node:child_process` |
| 89 | `execFileSync('git', [...])` — argv array, no shell). |
| 90 | 3. If the count is `0`, print "skills in sync with docs" and exit `0`. |
| 91 | 4. If `> 0`, print the list (`git diff --name-only <sha>..HEAD -- docs/`) and a |
| 92 | remediation message ("Re-sync `skills/` per `skills/GENERATION.md` and update |
| 93 | the `skills-generated-from` marker to the current HEAD"), then exit `1`. |
| 94 | Handle the case where `<sha>` is not an ancestor (e.g. shallow CI clone): |
| 95 | detect the failure of `git rev-list` and exit `0` with a warning rather than |
| 96 | hard-failing CI on a fetch-depth issue. |
| 97 | |
| 98 | Match the style of existing `scripts/*.mjs` (ESM, top-level await ok). |
| 99 | |
| 100 | ### Step 3: Wire it into CI |
| 101 | |
| 102 | Add a lightweight job (or a step in an existing job) that runs |
| 103 | `node scripts/check-skills-drift.mjs`. Because it needs history for |
| 104 | `git rev-list`, set the checkout to full depth: |
| 105 | ```yaml |
| 106 | skills-drift: |
| 107 | runs-on: ubuntu-latest |
| 108 | steps: |
| 109 | - uses: actions/checkout@v6 |
| 110 | with: |
| 111 | fetch-depth: 0 |
| 112 | - uses: actions/setup-node@v6 |
| 113 | with: |
| 114 | node-version: lts/* |
| 115 | - name: Check skills/docs drift |
| 116 | run: node scripts/check-skills-drift.mjs |
| 117 | ``` |
| 118 | |
| 119 | **Verify**: |
| 120 | - Locally, `node scripts/check-skills-drift.mjs` runs and exits non-zero **today** |
| 121 | (docs have advanced past `9d300814`), printing the changed docs files — that is |
| 122 | the guard working. (Do NOT "fix" it by editing skills in this plan; the point is |
| 123 | to surface the drift.) |
| 124 | - The workflow YAML parses. |
| 125 | |
| 126 | ## Test plan |
| 127 | |
| 128 | - Manual: run the script on the current tree → it reports drift (non-zero) and |
| 129 | lists changed `docs/` files. Temporarily set the marker to `HEAD`'s short SHA |
| 130 | and re-run → exits `0`. Restore the real recorded SHA afterward. |
| 131 | - No unit test framework needed for a small CI script; the two manual runs above |
| 132 | are the verification. |
| 133 | |
| 134 | ## Done criteria |
| 135 | |
| 136 | - [ ] `skills/GENERATION.md` has a `skills-generated-from:` marker |
| 137 | - [ ] `scripts/check-skills-drift.mjs` exists, uses argv-based `git` (no shell), and handles the non-ancestor/shallow case gracefully |
| 138 | - [ ] A CI job runs the check with `fetch-depth: 0` |
| 139 | - [ ] Running the script locally correctly reports the current drift (non-zero) |
| 140 | - [ ] Workflow YAML parses |
| 141 | - [ ] Only in-scope files modified (`git status`) |
| 142 | - [ ] `plans/README.md` status row updated |
| 143 | |
| 144 | ## STOP conditions |
| 145 | |
| 146 | Stop and report if: |
| 147 | |
| 148 | - The team wants the guard to also **auto-regenerate** skills (that's the |
| 149 | generator, direction item D2 — a separate plan; this one only detects drift). |
| 150 | - CI's default shallow clone can't be given full history — then base the check on |
| 151 | a different signal (e.g. compare a committed hash of `docs/` tree) and report |
| 152 | the approach change. |
| 153 | |
| 154 | ## Maintenance notes |
| 155 | |
| 156 | - When skills are re-synced, bump the `skills-generated-from:` marker (and the |
| 157 | human fields) to the current HEAD; the guard then goes green until docs move |
| 158 | again. |
| 159 | - Reviewer: confirm the script fails **loudly with guidance**, not silently, and |
| 160 | that it never edits generated files itself. |
| 161 |