Sync the project docs mount instead of hand-vendoring it #135

Closed
opened 2026-08-27 06:40:44 +00:00 by coilyco-ops · 1 comment
Collaborator

Angie's lane on coilysiren/inbox#438, split out because that bullet is a lane assignment inside a design brief rather than a trackable unit of work, and because the shape is now concrete: umbra's mount landed on main and the only thing keeping it noindex is that the copy is hand-made.

The gap

src/projects/umbra-docs/ holds 21 files copied by hand from coilyco-flight-deck/umbra at commit 2285380. Verified byte-identical at the time of copying, so the problem is not that it is wrong. The problem is that nothing keeps it right.

Indexing a drifting copy manufactures exactly the failure #133 names: if a page and a repository disagree about what a project is, the repository wins and the page is the bug. That would be 21 bugs on the canonical domain, so the mount stays out of the index until a sync exists.

Interim guard already in place: umbraDocsSource in src/data/umbra-docs.js records the source commit, and every docs page prints it and names the repository as the source of truth. That converts silent drift into visible drift. It is not a substitute for the sync.

What is already built, so this is smaller than it sounds

None of the hard parts are copy logic, and all of them are done and tested:

  • src/data/umbra-docs.js - the manifest. Section tree, reading order, sidebar, front door and prev/next all derive from it. documentation-layout permits no subdirectories under docs/, so this is the only place section structure can live.
  • src/projects/umbra-docs/umbra-docs.11tydata.js - permalink, title, shelf and reading position computed from the filename, so vendored files need no front matter and stay verbatim.
  • mountedDocLinks in eleventy.config.js - rewrites repo-relative .md links. 47 of them in umbra alone. A mounted target becomes its route, an unmounted one points back at the file in its own repo.
  • docs/project-docs-mount.md - the whole contract, including why the mount is excluded from dead-cross-links.

What is missing is the step that replaces cp with a fetch, plus writing one commit string.

Committed vendor, refreshed by a scheduled Forgejo Action. A workflow clones the source repo, copies docs/ through the exclusion list, updates umbraDocsSource, and commits.

  • The Eleventy build stays hermetic and offline, which is what local dev and CI both assume today.
  • Every content change arrives as a reviewable diff, which #438's "verbatim needs an exclusion list" requirement needs anyway.
  • It matches the house pattern already running in agentic-os-kai as sync-repo-registry.yml and sync-repo-digests.yml.

Rejected: fetching at build time, because local dev then diverges from CI unless a cached fallback exists, and the fallback is the vendored copy again. Rejected: submodules, because the exclusion list needs per-file control.

Carry these across

  • The exclusion list is required, not optional. #438 names the planning artifacts that must not mount: repository-plan.md, claude-native-ui-plan.md and three eval-ref-* pages in agent-compose, worklog.md and issues.md in sirens-echo.
  • Vendoring imports the source repo's link debt. Declaring a dead-cross-links exclude also flips that hook to a full-tree walk, which surfaced a pre-existing src/pages/resume.md link and pulled src/pages/** into the same list. The other three repos will hit this again.
  • umbra has three dead links in its own docs, found by mounting them. architecture.md points at features-detail.md, which does not exist. CONTRIBUTING.md points at CODE_OF_CONDUCT.md and SECURITY.md, which live at the repo root rather than under docs/. Worth fixing upstream rather than papering over here.

Done means

  • src/projects/umbra-docs/ is produced by a workflow rather than by hand
  • The exclusion list is declared in one place and applied per repo
  • umbraDocsSource is written by the same step that copies the files
  • A failed or stale sync is visible without opening the Actions tab
  • The mount can move to follow, index and into CANONICAL_ROUTES and llms.txt

That last box is not this issue's to tick. #133's done-means list also requires a Search Console baseline recorded before publication, which is Kai's.

Related: coilysiren/inbox#438, coilysiren/website#133.

Angie's lane on `coilysiren/inbox#438`, split out because that bullet is a lane assignment inside a design brief rather than a trackable unit of work, and because the shape is now concrete: umbra's mount landed on `main` and the only thing keeping it `noindex` is that the copy is hand-made. ## The gap `src/projects/umbra-docs/` holds 21 files copied by hand from `coilyco-flight-deck/umbra` at commit `2285380`. Verified byte-identical at the time of copying, so the problem is not that it is wrong. The problem is that nothing keeps it right. Indexing a drifting copy manufactures exactly the failure `#133` names: *if a page and a repository disagree about what a project is, the repository wins and the page is the bug.* That would be 21 bugs on the canonical domain, so the mount stays out of the index until a sync exists. Interim guard already in place: `umbraDocsSource` in `src/data/umbra-docs.js` records the source commit, and every docs page prints it and names the repository as the source of truth. That converts silent drift into visible drift. It is not a substitute for the sync. ## What is already built, so this is smaller than it sounds None of the hard parts are copy logic, and all of them are done and tested: * **`src/data/umbra-docs.js`** - the manifest. Section tree, reading order, sidebar, front door and prev/next all derive from it. `documentation-layout` permits no subdirectories under `docs/`, so this is the only place section structure can live. * **`src/projects/umbra-docs/umbra-docs.11tydata.js`** - permalink, title, shelf and reading position computed from the filename, so vendored files need no front matter and stay verbatim. * **`mountedDocLinks` in `eleventy.config.js`** - rewrites repo-relative `.md` links. 47 of them in umbra alone. A mounted target becomes its route, an unmounted one points back at the file in its own repo. * **`docs/project-docs-mount.md`** - the whole contract, including why the mount is excluded from `dead-cross-links`. What is missing is the step that replaces `cp` with a fetch, plus writing one commit string. ## Recommended shape **Committed vendor, refreshed by a scheduled Forgejo Action.** A workflow clones the source repo, copies `docs/` through the exclusion list, updates `umbraDocsSource`, and commits. * The Eleventy build stays hermetic and offline, which is what local dev and CI both assume today. * Every content change arrives as a reviewable diff, which `#438`'s "verbatim needs an exclusion list" requirement needs anyway. * It matches the house pattern already running in `agentic-os-kai` as `sync-repo-registry.yml` and `sync-repo-digests.yml`. Rejected: fetching at build time, because local dev then diverges from CI unless a cached fallback exists, and the fallback is the vendored copy again. Rejected: submodules, because the exclusion list needs per-file control. ## Carry these across * **The exclusion list is required, not optional.** `#438` names the planning artifacts that must not mount: `repository-plan.md`, `claude-native-ui-plan.md` and three `eval-ref-*` pages in agent-compose, `worklog.md` and `issues.md` in sirens-echo. * **Vendoring imports the source repo's link debt.** Declaring a `dead-cross-links` exclude also flips that hook to a full-tree walk, which surfaced a pre-existing `src/pages/resume.md` link and pulled `src/pages/**` into the same list. The other three repos will hit this again. * **umbra has three dead links in its own docs**, found by mounting them. `architecture.md` points at `features-detail.md`, which does not exist. `CONTRIBUTING.md` points at `CODE_OF_CONDUCT.md` and `SECURITY.md`, which live at the repo root rather than under `docs/`. Worth fixing upstream rather than papering over here. ## Done means - [ ] `src/projects/umbra-docs/` is produced by a workflow rather than by hand - [ ] The exclusion list is declared in one place and applied per repo - [ ] `umbraDocsSource` is written by the same step that copies the files - [ ] A failed or stale sync is visible without opening the Actions tab - [ ] The mount can move to `follow, index` and into `CANONICAL_ROUTES` and `llms.txt` That last box is not this issue's to tick. `#133`'s done-means list also requires a Search Console baseline recorded before publication, which is Kai's. Related: `coilysiren/inbox#438`, `coilysiren/website#133`.
Author
Collaborator

Landed on main. Full pipeline green on 20bd2fd: test, test-e2e, mirror, staging image, TruffleHog.

Done means

  • src/projects/umbra-docs/ is produced by a workflow. scripts/sync-project-docs.py clones each source shallow, copies what the exclusion list permits, and writes the stamp. Running it against the hand copy changed nothing: 21 files, byte-identical, at the commit the stamp already named, which is the strongest evidence available that it reproduces what was there.
  • The exclusion list is declared in one place. src/data/docs-mounts.json carries source, target, and exclusions per project, plus the lists for the repos not mounted yet, so #438's must-not-mount set survives until agent-compose and sirens-echo land.
  • umbraDocsSource is written by the same step that copies the files. It is generated into src/data/docs-mount-source.json and joined in at src/_data/umbraDocs.js, so the manifest went back to pure hand-written structure and a generated file no longer shares a path with a reviewed one.
  • A failed or stale sync is visible without the Actions tab. A failing run alerts Telegram on the existing path. A run that stops happening ages the stamp, and src/docs-mount.test.ts fails the ordinary gate at 14 days. Both forced and observed: backdating gives umbra was last synced 57 days ago, and an unlisted file in the mount fails the manifest-agreement check.
  • Index and CANONICAL_ROUTES. Left as filed. Drift is no longer what holds it, so what remains is #133's Search Console baseline, which is Kai's.

Proven rather than assumed

Dispatched the workflow on the real runner (run 629, success):

umbra: 2285380 (2026-08-26), 0 file(s) changed, commit unchanged
project docs unchanged

Checkout token, egress to the source repo, and the exit-without-pushing path all work.

Two things found on the way

test-e2e was already red before this, on the parent commit 7ffe7c2 and every one of the 21 docs pages: landmark-main-is-top-level, landmark-no-duplicate-main, landmark-unique. base.njk opens the page's <main> and the docs frame opened a second inside it. So the mount shipped with an accessibility failure that its own coverage was reporting to nobody. Fixed in 4a7b2cb: the inner element is a div keeping the id the skip link targets, with tabindex="-1" so the skip moves focus. Cypress went from 22 passing and 21 failing to 89 passing.

The sync runs on the runner, not through scripts/ci/run-in-container.sh. That adapter streams the checkout in with tar | docker run, so files written inside the container never come back. Worth knowing before anyone adds a second write-back job. Git and python3 are all this needs, which is what mirror-to-github.sh already assumes.

Also: code-comments reads tracked files, so a new file passes --all-files while untracked and fails on the commit after the one that lands it. That cost 20bd2fd.

Landed on `main`. Full pipeline green on 20bd2fd: test, test-e2e, mirror, staging image, TruffleHog. ## Done means - [x] `src/projects/umbra-docs/` is produced by a workflow. `scripts/sync-project-docs.py` clones each source shallow, copies what the exclusion list permits, and writes the stamp. Running it against the hand copy changed **nothing**: 21 files, byte-identical, at the commit the stamp already named, which is the strongest evidence available that it reproduces what was there. - [x] The exclusion list is declared in one place. `src/data/docs-mounts.json` carries source, target, and exclusions per project, plus the lists for the repos not mounted yet, so #438's must-not-mount set survives until agent-compose and sirens-echo land. - [x] `umbraDocsSource` is written by the same step that copies the files. It is generated into `src/data/docs-mount-source.json` and joined in at `src/_data/umbraDocs.js`, so the manifest went back to pure hand-written structure and a generated file no longer shares a path with a reviewed one. - [x] A failed or stale sync is visible without the Actions tab. A failing run alerts Telegram on the existing path. A run that stops happening ages the stamp, and `src/docs-mount.test.ts` fails the ordinary gate at 14 days. Both forced and observed: backdating gives `umbra was last synced 57 days ago`, and an unlisted file in the mount fails the manifest-agreement check. - [ ] Index and `CANONICAL_ROUTES`. Left as filed. Drift is no longer what holds it, so what remains is #133's Search Console baseline, which is Kai's. ## Proven rather than assumed Dispatched the workflow on the real runner (run 629, success): ``` umbra: 2285380 (2026-08-26), 0 file(s) changed, commit unchanged project docs unchanged ``` Checkout token, egress to the source repo, and the exit-without-pushing path all work. ## Two things found on the way **`test-e2e` was already red before this**, on the parent commit 7ffe7c2 and every one of the 21 docs pages: `landmark-main-is-top-level`, `landmark-no-duplicate-main`, `landmark-unique`. `base.njk` opens the page's `<main>` and the docs frame opened a second inside it. So the mount shipped with an accessibility failure that its own coverage was reporting to nobody. Fixed in 4a7b2cb: the inner element is a `div` keeping the id the skip link targets, with `tabindex="-1"` so the skip moves focus. Cypress went from 22 passing and 21 failing to 89 passing. **The sync runs on the runner, not through `scripts/ci/run-in-container.sh`.** That adapter streams the checkout in with `tar | docker run`, so files written inside the container never come back. Worth knowing before anyone adds a second write-back job. Git and python3 are all this needs, which is what `mirror-to-github.sh` already assumes. Also: `code-comments` reads tracked files, so a new file passes `--all-files` while untracked and fails on the commit after the one that lands it. That cost 20bd2fd.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilysiren/website#135
No description provided.