diff --git a/.github/workflows/coherence.yml b/.github/workflows/coherence.yml new file mode 100644 index 0000000..b9995ae --- /dev/null +++ b/.github/workflows/coherence.yml @@ -0,0 +1,16 @@ +name: Coherence + +on: + pull_request: + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node scripts/check-coherence.mjs diff --git a/CLAUDE.md b/CLAUDE.md index c40c6a2..2f960f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,4 +35,7 @@ Non-negotiables: (cloud/self-hosted, fresh/upgrade, existing agents, subscriptions). - Findings rejected by Seb (`references/decisions.md`) are never re-raised unless the platform changed again on that point. +- Every page exists twice: `.mdx` at the repo root, published by + Mintlify, and `llm-gateway/docs/.mdx`, its dead twin. Edit both, or + the change never reaches the site. The coherence check enforces it. - Invariants are machine-checked: `node scripts/check-coherence.mjs` must be GREEN before any issue is filed or any docs PR is pushed. Reversing a recorded decision requires the written derivation of the reversal protocol (see the docs-sync skill). diff --git a/scripts/check-coherence.mjs b/scripts/check-coherence.mjs index 379ef5a..3cb81f6 100644 --- a/scripts/check-coherence.mjs +++ b/scripts/check-coherence.mjs @@ -6,6 +6,10 @@ * must have its row in reference/environment-variables.mdx. * 2. ERROR-CODE invariant: any M### code cited in a docs page must * have its page at errors/M###.mdx. + * 3. TWIN invariant: every page exists twice, at the repo root and under + * llm-gateway/docs/. The root copy is the published one. The two must + * stay identical, so that a correction made in the wrong file is caught + * here instead of silently changing nothing on the site. * * Usage: node scripts/check-coherence.mjs * Exit 0 = GREEN, 1 = violations (each printed with file:line). @@ -100,6 +104,32 @@ for (const f of files) { }); } +// --- Twin invariant -------------------------------------------------------- +// Mintlify publishes the root copy. The copy under llm-gateway/docs/ is dead +// weight kept for now, and an edit made there alone changes nothing on the +// site, so the two must match byte for byte. +const TWIN_DIR = join(ROOT, "llm-gateway/docs"); +if (existsSync(TWIN_DIR)) { + const twins = walk(TWIN_DIR).map((f) => relative(TWIN_DIR, f)); + for (const page of twins) { + const root = join(ROOT, page); + if (!existsSync(root)) { + violations.push(`llm-gateway/docs/${page} has no twin at the repo root, so it is never published`); + continue; + } + if (readFileSync(root, "utf8") !== readFileSync(join(TWIN_DIR, page), "utf8")) { + violations.push(`${page} differs from llm-gateway/docs/${page} — edit both, the root copy is the published one`); + } + } + for (const f of walk(ROOT)) { + const rel = relative(ROOT, f); + if (rel.startsWith("llm-gateway/") || !rel.endsWith(".mdx")) continue; + if (!existsSync(join(TWIN_DIR, rel))) { + violations.push(`${rel} has no twin under llm-gateway/docs/`); + } + } +} + const unique = [...new Set(violations)]; if (unique.length === 0) { console.log(`Coherence: GREEN (${files.length} pages, ${defined.size} vars defined)`);