Skip to content

ADR-0020 — Auto-render plumbing for universe page, ADR index, and CLAUDE.md

Field Value
Status Accepted
Date 2026-05-23
Supersedes
Superseded by
Related ADR-0006 (ops site as rendered standing-reference surface); ADR-0019 (same session, phase-1.5 batch)

Context

Three surfaces on the ops site (ops.esg-screen.org) were hand-maintained and drifting from their source of truth:

  1. /architecture/universeesg-screen-ops/docs/architecture/universe.md, hand-maintained. Source of truth = institution table in the production DB. After cycle 15 expanded the universe from 22 to 76 institutions (2026-05-22), the page was confirmed stale: 22 institutions, cycle-11-era count and notes.

  2. /decisions/*esg-screen-ops/docs/decisions/, partially hand-maintained. The index table in index.md showed only ADRs 0001–0006, 0012–0013. ADRs 0007–0019 were missing. Individual ADR files for most ADRs were absent. ADR-0006 recorded the original intent as "direct render from docs/adr/" but the plumbing was never built.

  3. /reference/claudemd — not built at all. ADR-0006 listed /reference/claudemd as a planned surface rendering from .claude/CLAUDE.md; the page did not exist.

These three share the same root cause: no mechanism to propagate changes from the source of truth into the ops site without manual intervention. The class of drift is documented in the backlog as the "handoff verification / Pages rebuild miss" item, with three confirmed live-site-vs-repo drift incidents attributed to the same manual-copy pattern.

This batch was backlogged as the "auto-render bundle" (Phase 1.5, 2026-05-22).


Decision

Build three scripts in esg-screening/scripts/ and wire them:

Script Source of truth Target
generate-universe-md.js institution table in data/esg.db esg-screen-ops/docs/architecture/universe.md
sync-adr-to-ops.js docs/adr/*.md in this repo esg-screen-ops/docs/decisions/*.md + index table + nav
sync-claudemd-to-ops.js .claude/CLAUDE.md in this repo esg-screen-ops/docs/reference/claudemd.md + nav

Approach: committed copy, not build-time fetch.

The ops site is deployed from the esg-screen-ops repo via Cloudflare Pages. Pages builds mkdocs build from the committed markdown. The two options that avoid a network dependency at Pages build time are:

  • Committed copy (chosen): scripts run on the VM, read from both repos (which co-exist on the same filesystem), and write generated markdown into the ops repo. The ops repo commit is what Pages deploys — no network call at build time.
  • Submodule: ops repo has esg-screening as a submodule; Pages clones both. Clean in principle but adds setup complexity and depends on GitHub submodule support in the Pages build environment. Not justified at current scale.
  • Build-time fetch: Pages fetches source files from GitHub during mkdocs build. Rejected: network dependency that can fail silently, breaking the static build.

Committed copy is the right call for a same-VM setup: simpler, no new external dependency, and the VM is the natural place to run both repos' tooling.

Trigger — universe page:

generate-universe-md.js is called as the final step in src/scrapers/run.js, after the scrape_run row is closed to complete/partial. The post-run hook ensures the universe page is regenerated after every scoring run, which is the natural moment when institution state may have changed.

The generator writes directly to the ops repo on disk. Committing and pushing the ops repo is a separate step (session-close workflow), not automatic on each run.

Trigger — ADR index and CLAUDE.md:

sync-adr-to-ops.js and sync-claudemd-to-ops.js are run manually at session close when ADRs or CLAUDE.md have changed. They are not wired to a cron or git hook — the change cadence is low enough that a manual trigger per CC-on-VM session is sufficient.


Rationale

Universe page trigger in run.js: the scoring run is the natural event boundary for institution state changes. Post-run is the right moment; pre-run would generate a stale snapshot. A cron independent of the scoring run would introduce a separate trigger that can drift from actual DB state. A manual trigger puts it back in the hand-maintenance category.

Committed copy over submodule: submodule setup requires ops repo changes, Pages configuration verification, and ongoing maintenance if the screening repo's default branch changes. The committed-copy scripts do the same job with less infrastructure. If the repos move off the same VM, a submodule becomes a better option — that's a fresh decision at that point.

Single-script-per-concern: three scripts rather than one combined script makes each independently runnable and testable without side effects on the others.


Consequences

  • Universe page is re-generated on every scoring run. Next post-run ops commit will always reflect the current institution table. No manual universe page updates needed; the ops commit cadence (weekly or after cycle work) is sufficient.
  • ADR index on the ops site will include all ADRs and remain current as long as sync-adr-to-ops.js is run at session close when ADRs are added.
  • CLAUDE.md is rendered on the ops site, closing the /reference/claudemd gap from ADR-0006.
  • Payoff from first run: universe page regenerated from 22 → 76 institutions (cycle 15 expansion), confirming the drift that motivated this ADR.
  • The hand-maintenance burden on the three surfaces is eliminated. The residual manual step (committing the ops repo) is low-friction and already part of the session-close workflow.
  • Scripts are VM-path-coupled (../../esg-screen-ops/... relative to this repo). If the repos move, update the path constants in each script.

Alternatives considered

Build-time fetch from GitHub API: Pages build script fetches ADR files via GitHub raw URLs. Rejected — network dependency at build time, breaks on rate limits or GitHub outages, leaks internal repo structure to build logs.

Submodule: esg-screen-ops has esg-screening as a git submodule. Pages clones both. Cleaner conceptually; deferred because it requires non-trivial Pages configuration and the same-VM committed-copy pattern achieves the goal without it.

Manual copy on each cycle: the status quo. Confirmed broken — three drift incidents already recorded, and the cycle-15 universe expansion would have gone unnoticed until a chat session tried to read the page.


References

  • ADR-0006: ops site as rendered standing-reference surface; original intent table lists /decisions/NNNN-* and /reference/claudemd as planned auto-render targets
  • Backlog "auto-render bundle" (Phase 1.5, deferred 2026-05-22)
  • Backlog "handoff verification / Pages rebuild miss" (three confirmed drift incidents)
  • Cycle 15 universe expansion: 22 → 76 institutions (2026-05-22, migrations 044–049)