Skip to content

Plan: Docs framework — Starlight + llms.txt + auto-gen implementation

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

How-to — active implementation plan. Moves to ../archive/ when complete.

  • Status: not started (2026-04-21)
  • Owner: solo (Claude Code session)
  • Spec: docs/specs/2026-04-21-docs-framework-design.md
  • ADR: docs/decisions/0027-docs-framework-starlight.md

Ship an agent-native docs stack on top of the existing Diátaxis-organized docs/ tree: Starlight renderer at apps/docs/ serving docs.ideony.is-a.dev, llms.txt + llms-full.txt for agent consumption, OpenAPI / Prisma / env auto-generation, layered quality gates (markdownlint / vale / cspell / lychee / frontmatter), and an upgraded zero-drift check-docs-updated.sh. Cofounder-gated via Cloudflare Access; parallel with the existing Drive doc.

Raw Markdown in docs/ stays the single source of truth. apps/docs/ is a Starlight Astro app that content-loads ../../docs/**/*.md — no copy, no drift. docs/_generated/** holds CI-written MD for API / DB / env / CLI — gitignored, regenerated pre-build. Quality gates run in pre-commit via Husky + lint-staged and in CI via docs-quality.yml. Hosting on Cloudflare Pages with Zero Trust email allowlist.

  • Astro 5 + Starlight 0.30+ (@astrojs/starlight)
  • Plugins: starlight-llms-txt, starlight-openapi, rehype-mermaid, starlight-versions (inactive)
  • Pagefind (bundled with Starlight) for static search
  • Quality: markdownlint-cli2, vale, cspell, lychee, @mermaid-js/mermaid-cli, ajv (custom frontmatter validator)
  • Auto-gen: widdershins (OpenAPI → MD), prisma-markdown, prisma-erd-generator
  • Host: Cloudflare Pages + Cloudflare Zero Trust (email allowlist)
  • CI: GitHub Actions (docs-quality.yml, docs-deploy.yml)

Phase 1 (shell)
Phase 2 (agent layer) ← depends on Phase 1
Phase 3 (auto-gen pipelines) ← depends on Phase 2 (frontmatter schema)
Phase 4 (quality gates local) ← depends on Phase 3 (gates include _generated/)
Phase 5 (zero-drift audit) ║ Phase 6 (diagram migration) ← parallel, disjoint file sets
Phase 7 (cofounder migration) — async, non-blocking
Phase 4.5 (living-examples CI) — gated on Phase 4 running clean ≥ 1 week
Phase 8 (federation prep) — deferred until Jooice docs init

Commit frequently (one per task where possible). Each phase ends with a status.md update + CHANGELOG under [Unreleased].


Goal: apps/docs/ Astro-Starlight app live locally, content-loading docs/**/*.md, deployed to Cloudflare Pages behind Zero Trust.

Files:

  • Create: apps/docs/package.json

  • Create: apps/docs/astro.config.mjs

  • Create: apps/docs/tsconfig.json

  • Create: apps/docs/.gitignore

  • Create: apps/docs/src/content.config.ts

  • Create: apps/docs/README.md

  • Modify: pnpm-workspace.yaml (already globs apps/*, verify)

  • Step 1: Look up current LTS Astro + Starlight versions via Context7

Run:

mcp__plugin_context7_context7__resolve-library-id("astro")
mcp__plugin_context7_context7__query-docs("astro integrations starlight latest stable")

Record versions in plan below. Expected: Astro ≥ 5.x, Starlight ≥ 0.30.x.

  • Step 2: Create apps/docs/package.json
{
"name": "docs",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev --port 4321",
"build": "astro check && astro build",
"preview": "astro preview",
"clean": "rm -rf dist .astro node_modules/.cache"
},
"dependencies": {
"astro": "<PIN-to-latest-stable>",
"@astrojs/starlight": "<PIN-to-latest-stable>",
"sharp": "<PIN-to-latest-stable>"
},
"devDependencies": {
"typescript": "^5.6.0"
},
"engines": {
"node": ">=22.0.0"
}
}

Replace <PIN-to-latest-stable> with versions from Step 1. Enforce ADR 0025 (LTS / stable only — no canary, beta, rc).

  • Step 3: Create apps/docs/astro.config.mjs
// @ts-check
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
export default defineConfig({
site: "https://docs.ideony.is-a.dev",
trailingSlash: "never",
integrations: [
starlight({
title: "Ideony",
description: "Skilled-trades marketplace — internal docs",
logo: { src: "./src/assets/logo.svg", replacesTitle: false },
social: [
{ icon: "github", label: "GitHub", href: "https://github.com/aciDrums7/ideony" },
],
customCss: ["./src/styles/custom.css"],
sidebar: [
{ label: "Start here", link: "/getting-started" },
{ label: "Architecture", link: "/architecture" },
{ label: "Design system", link: "/design-system" },
{ label: "Infrastructure", link: "/infrastructure" },
{ label: "Testing", link: "/testing" },
{ label: "Roadmap", link: "/roadmap" },
{ label: "Status", link: "/status" },
{ label: "Glossary", link: "/glossary" },
{ label: "Decisions (ADRs)", autogenerate: { directory: "decisions" } },
{ label: "Specs", autogenerate: { directory: "specs" } },
{ label: "Plans", autogenerate: { directory: "plans" } },
{ label: "Research", autogenerate: { directory: "research" } },
],
}),
],
});
  • Step 4: Create apps/docs/src/content.config.ts
import { defineCollection } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
// Content loader points at repo-root docs/ — raw MD is SSoT, not copied.
export const collections = {
docs: defineCollection({
loader: docsLoader({ base: "../../docs" }),
schema: docsSchema(),
}),
};
  • Step 5: Create apps/docs/tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
  • Step 6: Create apps/docs/.gitignore
dist
.astro
node_modules
.env
  • Step 7: Create placeholder logo + custom CSS
Terminal window
mkdir -p apps/docs/src/assets apps/docs/src/styles

apps/docs/src/assets/logo.svg — copy existing Ideony mark from apps/mobile/assets/ (if present) or a temporary 32×32 SVG with the terracotta color.

apps/docs/src/styles/custom.css:

/* Italian Sole palette — cross-reference docs/design-system.md */
:root {
--sl-color-accent: #b35f3b; /* terracotta */
--sl-color-accent-high: #e89059; /* sun amber */
--sl-color-bg: #faf6ee; /* cream */
--sl-color-text: #2b1e10; /* dark ink */
}
:root[data-theme="dark"] {
--sl-color-accent: #e89059;
--sl-color-accent-high: #b35f3b;
--sl-color-bg: #1a120a;
--sl-color-text: #faf6ee;
}
  • Step 8: Create apps/docs/README.md
# apps/docs — Starlight renderer
Renders [`docs/`](../../docs/) as a static site. Raw MD lives in `docs/` (SSoT). This app is a **view**, not a store.
## Commands
```bash
pnpm -F docs dev # local preview on http://localhost:4321
pnpm -F docs build # static build → dist/
pnpm -F docs preview # preview built site

Cloudflare Pages project ideony-docsdocs.ideony.is-a.dev. See docs/infrastructure.md.

- [ ] **Step 9: Install + verify local dev**
Run:
```bash
pnpm install
pnpm -F docs dev

Expected: Astro dev server starts on http://localhost:4321, landing page renders.

  • Step 10: Check build passes

Run:

Terminal window
pnpm -F docs build

Expected: dist/ folder produced with static HTML, no errors.

  • Step 11: Commit
Terminal window
git add apps/docs pnpm-workspace.yaml pnpm-lock.yaml
git commit -m "feat(docs): scaffold apps/docs Starlight renderer"

Docs gate note: touches apps/docs/ (new); CLAUDE.md update happens in Task 1.6.


Files:

  • Modify: turbo.json

  • Modify: root package.json (scripts)

  • Step 1: Read current turbo.json

Record current tasks or pipeline keys.

  • Step 2: Add docs:gen and docs:build tasks
{
"tasks": {
"docs:gen": {
"outputs": ["docs/_generated/**"]
},
"docs:build": {
"dependsOn": ["^build", "docs:gen"],
"outputs": ["apps/docs/dist/**"],
"inputs": [
"docs/**/*.md",
"docs/_generated/**",
"apps/docs/**",
"!apps/docs/dist/**",
"!apps/docs/.astro/**",
"!apps/docs/node_modules/**"
]
}
}
}

(Merge with existing config, don’t overwrite.)

  • Step 3: Add root script aliases

In root package.json:

{
"scripts": {
"docs:dev": "pnpm -F docs dev",
"docs:build": "turbo run docs:build",
"docs:gen": "turbo run docs:gen"
}
}
  • Step 4: Verify turbo picks up tasks

Run:

Terminal window
pnpm turbo run docs:build --dry-run=json | jq '.tasks[] | select(.taskId | startswith("docs:"))'

Expected: two tasks listed (docs:gen, docs:build for apps/docs).

  • Step 5: Commit
Terminal window
git add turbo.json package.json
git commit -m "chore(docs): wire docs:gen + docs:build into turbo pipeline"

Files:

  • Modify: root .gitignore

  • Step 1: Add entry

Append to .gitignore:

# Auto-generated docs (CI writes these before Starlight build)
docs/_generated/
  • Step 2: Commit
Terminal window
git add .gitignore
git commit -m "chore(docs): gitignore docs/_generated (CI-written)"

Task 1.4: Verify every docs/**/*.md renders without Starlight frontmatter errors

Section titled “Task 1.4: Verify every docs/**/*.md renders without Starlight frontmatter errors”

Files (read-only for this task): all docs/*.md, docs/decisions/*.md, docs/specs/*.md, docs/plans/*.md, docs/research/*.md, docs/archive/**/*.md.

  • Step 1: Run build + capture errors

Run:

Terminal window
pnpm -F docs build 2>&1 | tee /tmp/starlight-build.log
  • Step 2: Grep for errors

Run:

Terminal window
grep -E "(error|warning)" /tmp/starlight-build.log
  • Step 3: For each failing file, fix minimally

Most common failures + fixes (apply per-file):

  • Starlight requires title: in frontmatter. If a doc has no frontmatter, prepend:
    ---
    title: <Derive from first H1>
    ---
  • If frontmatter has title: but not YAML, wrap in --- fences.
  • If MD contains raw < that Astro MDX rejects, escape as &lt; or wrap in code fence.

Do NOT change content; only minimally unblock rendering.

  • Step 4: Rerun build to green
Terminal window
pnpm -F docs build

Expected: zero errors.

  • Step 5: Commit
Terminal window
git add docs
git commit -m "chore(docs): minimal frontmatter fixes for Starlight build"

Task 1.5: Cloudflare Pages project + DNS + Zero Trust gate

Section titled “Task 1.5: Cloudflare Pages project + DNS + Zero Trust gate”

Files:

  • Modify: docs/infrastructure.md (add a “Docs site” section)

  • Step 1: Cloudflare Pages project

Via Cloudflare dashboard OR Wrangler CLI (wrangler pages project create ideony-docs), create project ideony-docs. Build command: pnpm -F docs build. Output dir: apps/docs/dist. Root dir: repo root.

  • Step 2: Connect repo

GitHub → Cloudflare Pages: connect aciDrums7/ideony, branch main, trigger on push.

  • Step 3: Custom domain

Add docs.ideony.is-a.dev as custom domain. Wait for ideony.is-a.dev zone active (per CLAUDE.md, pending is-a-dev/register#36614). If still pending, use fallback *.pages.dev URL temporarily.

  • Step 4: Cloudflare Zero Trust policy

  • Create Zero Trust application: name ideony-docs, type self-hosted, domain docs.ideony.is-a.dev.

  • Policy: Allow — include: emails in allowlist (add cofounder + team Google Workspace emails).

  • Authentication method: Google OAuth + one-time email PIN.

  • Session duration: 24h.

  • Audit logging: on.

  • Step 5: Verify access

  • In an incognito browser, visit https://docs.ideony.is-a.dev.

  • Expected: CF Access login screen; after auth, docs site renders.

  • Non-allowlisted email: access denied page.

  • Step 6: Document in docs/infrastructure.md

Add a new section “Docs site” describing: CF Pages project, custom domain, Zero Trust policy, build command. Cross-reference ADR 0027 + spec.

  • Step 7: Commit
Terminal window
git add docs/infrastructure.md
git commit -m "docs(infra): document Cloudflare Pages + Zero Trust for docs site"

Task 1.6: Update CLAUDE.md + docs/README.md to reference new site

Section titled “Task 1.6: Update CLAUDE.md + docs/README.md to reference new site”

Files:

  • Modify: CLAUDE.md

  • Modify: docs/README.md

  • Modify: CHANGELOG.md

  • Modify: docs/status.md

  • Step 1: Add site URL to CLAUDE.md

Under the “Documentation” section:

## Documentation
Canonical docs live in [`docs/`](./docs/). Entry point: [`docs/README.md`](./docs/README.md) — sitemap with Diátaxis quadrants.
**Rendered site:** [docs.ideony.is-a.dev](https://docs.ideony.is-a.dev) — Starlight renderer of `docs/` (cofounder + team gated via CF Access). Raw MD is SSoT; site is a view.
  • Step 2: Add site URL to docs/README.md

Under the H1:

# Ideony — Documentation
_Sitemap and reading order. Every doc is tagged with a Diátaxis quadrant so its role stays unambiguous._
**Rendered view:** [`docs.ideony.is-a.dev`](https://docs.ideony.is-a.dev) — Starlight site, cofounder + team gated.
  • Step 3: Add CHANGELOG entry under [Unreleased]
### Added
- Starlight docs renderer at `apps/docs/`, deployed to `docs.ideony.is-a.dev` behind Cloudflare Zero Trust.
  • Step 4: Add to docs/status.md “In flight” section
- Docs framework — Phase 1 (renderer shell) shipped; Phase 2 (agent layer) next. Spec: `docs/specs/2026-04-21-docs-framework-design.md`. Plan: `docs/plans/2026-04-21-docs-framework-impl.md`.
  • Step 5: Commit
Terminal window
git add CLAUDE.md docs/README.md CHANGELOG.md docs/status.md
git commit -m "docs: reference rendered Starlight site across top-level docs"

Goal: /llms.txt + /llms-full.txt live; ?format=md raw-source URLs; every docs/**/*.md tagged with frontmatter schema (quadrant, audience, last_reviewed).

Task 2.1: Install + configure starlight-llms-txt

Section titled “Task 2.1: Install + configure starlight-llms-txt”

Files:

  • Modify: apps/docs/package.json

  • Modify: apps/docs/astro.config.mjs

  • Step 1: Look up latest starlight-llms-txt version via Context7 or npm

Run:

Terminal window
npm view starlight-llms-txt version

Pin to stable (not next / alpha).

  • Step 2: Add dep
Terminal window
pnpm -F docs add starlight-llms-txt@<latest-stable>
  • Step 3: Wire plugin

In apps/docs/astro.config.mjs, import and add to starlight({ plugins: [...] }):

import starlightLlmsTxt from "starlight-llms-txt";
// inside starlight({...})
plugins: [
starlightLlmsTxt({
projectName: "Ideony",
description: "Skilled-trades marketplace — internal engineering docs",
exclude: ["archive/**"],
}),
],
  • Step 4: Build + verify outputs
Terminal window
pnpm -F docs build
test -f apps/docs/dist/llms.txt && echo "OK llms.txt"
test -f apps/docs/dist/llms-full.txt && echo "OK llms-full.txt"

Expected: both files present, non-empty.

  • Step 5: Inspect llms.txt sanity
Terminal window
head -20 apps/docs/dist/llms.txt
wc -l apps/docs/dist/llms-full.txt

Expected: llms.txt has project header + section links; llms-full.txt > 1000 lines.

  • Step 6: Commit
Terminal window
git add apps/docs/package.json apps/docs/astro.config.mjs pnpm-lock.yaml
git commit -m "feat(docs): generate /llms.txt + /llms-full.txt (starlight-llms-txt)"

Task 2.2: Define frontmatter schema + validator

Section titled “Task 2.2: Define frontmatter schema + validator”

Files:

  • Create: scripts/validate-frontmatter.mjs

  • Create: scripts/validate-frontmatter.test.mjs

  • Modify: package.json (add script)

  • Step 1: Write the failing test

Create scripts/validate-frontmatter.test.mjs:

import { test } from "node:test";
import assert from "node:assert/strict";
import { validateDoc } from "./validate-frontmatter.mjs";
test("valid doc passes", () => {
const md = `---
title: Architecture
quadrant: reference
audience: dev
last_reviewed: 2026-04-21
---
# Architecture`;
const r = validateDoc("docs/architecture.md", md);
assert.equal(r.ok, true);
});
test("missing quadrant fails", () => {
const md = `---
title: Foo
audience: dev
---
# Foo`;
const r = validateDoc("docs/foo.md", md);
assert.equal(r.ok, false);
assert.match(r.errors[0], /quadrant/);
});
test("invalid quadrant fails", () => {
const md = `---
title: Foo
quadrant: nonsense
audience: dev
last_reviewed: 2026-04-21
---
# Foo`;
const r = validateDoc("docs/foo.md", md);
assert.equal(r.ok, false);
});
test("invalid audience fails", () => {
const md = `---
title: Foo
quadrant: reference
audience: marketing
last_reviewed: 2026-04-21
---
# Foo`;
const r = validateDoc("docs/foo.md", md);
assert.equal(r.ok, false);
});
test("archive files exempt", () => {
const md = `# Old doc without frontmatter`;
const r = validateDoc("docs/archive/2026-04-01/foo.md", md);
assert.equal(r.ok, true);
});
  • Step 2: Run test, expect fail
Terminal window
node --test scripts/validate-frontmatter.test.mjs

Expected: all tests fail (validateDoc undefined).

  • Step 3: Write minimal validate-frontmatter.mjs
import { readFileSync } from "node:fs";
import { parse } from "yaml";
const QUADRANTS = ["tutorial", "how-to", "reference", "explanation", "mixed"];
const AUDIENCES = ["agent", "dev", "cofounder", "public"];
const EXEMPT_PREFIXES = ["docs/archive/", "docs/_generated/"];
export function validateDoc(path, content) {
if (EXEMPT_PREFIXES.some((p) => path.startsWith(p))) return { ok: true, errors: [] };
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return { ok: false, errors: [`${path}: missing frontmatter block`] };
let fm;
try {
fm = parse(match[1]);
} catch (e) {
return { ok: false, errors: [`${path}: invalid YAML frontmatter: ${e.message}`] };
}
const errors = [];
if (!fm.title || typeof fm.title !== "string") errors.push(`${path}: title missing or not a string`);
if (!fm.quadrant || !QUADRANTS.includes(fm.quadrant))
errors.push(`${path}: quadrant must be one of ${QUADRANTS.join(", ")}`);
if (!fm.audience || !AUDIENCES.includes(fm.audience))
errors.push(`${path}: audience must be one of ${AUDIENCES.join(", ")}`);
if (!fm.last_reviewed || !/^\d{4}-\d{2}-\d{2}$/.test(String(fm.last_reviewed)))
errors.push(`${path}: last_reviewed must be YYYY-MM-DD`);
return { ok: errors.length === 0, errors };
}
// CLI entry: validate files passed as args
if (import.meta.url === `file://${process.argv[1]}`) {
const files = process.argv.slice(2);
let failed = 0;
for (const file of files) {
const r = validateDoc(file, readFileSync(file, "utf8"));
if (!r.ok) {
for (const err of r.errors) console.error(err);
failed++;
}
}
if (failed > 0) {
console.error(`\n${failed} file(s) failed frontmatter validation`);
process.exit(1);
}
}
  • Step 4: Install yaml dep at root
Terminal window
pnpm add -w -D yaml@^2.5.0
  • Step 5: Run test, expect pass
Terminal window
node --test scripts/validate-frontmatter.test.mjs

Expected: all 5 tests pass.

  • Step 6: Add root script

In root package.json:

{
"scripts": {
"docs:validate-frontmatter": "node scripts/validate-frontmatter.mjs docs/*.md docs/decisions/*.md docs/specs/*.md docs/plans/*.md docs/research/*.md"
}
}
  • Step 7: Commit
Terminal window
git add scripts/validate-frontmatter.mjs scripts/validate-frontmatter.test.mjs package.json pnpm-lock.yaml
git commit -m "feat(docs): frontmatter schema validator (TDD, 5 tests)"

Task 2.3: Backfill frontmatter on existing docs

Section titled “Task 2.3: Backfill frontmatter on existing docs”

Files: all docs/*.md, docs/decisions/*.md, docs/specs/*.md, docs/plans/*.md, docs/research/*.md.

  • Step 1: Run validator to get failure list
Terminal window
pnpm docs:validate-frontmatter || true

Capture all error messages.

  • Step 2: Map each file to quadrant + audience

Reference docs/README.md quadrant table. Defaults:

  • docs/getting-started.mdquadrant: tutorial, audience: dev

  • docs/architecture.mdquadrant: mixed, audience: dev

  • docs/design-system.mdquadrant: reference, audience: dev

  • docs/infrastructure.mdquadrant: mixed, audience: dev

  • docs/testing.mdquadrant: mixed, audience: dev

  • docs/roadmap.mdquadrant: explanation, audience: cofounder

  • docs/status.mdquadrant: reference, audience: dev

  • docs/glossary.mdquadrant: reference, audience: dev

  • docs/README.mdquadrant: explanation, audience: dev

  • docs/decisions/*.mdquadrant: explanation, audience: dev

  • docs/specs/*.mdquadrant: explanation, audience: dev

  • docs/plans/*.mdquadrant: how-to, audience: dev

  • docs/research/*.mdquadrant: reference, audience: dev

  • Step 3: For each file, add frontmatter block at top

Example for docs/architecture.md:

---
title: Architecture
quadrant: mixed
audience: dev
last_reviewed: 2026-04-21
---

If the file already has a frontmatter block (e.g., specs already have Status:, Date: lines in a pseudo-frontmatter), merge into a proper YAML block — don’t create duplicates.

  • Step 4: Rerun validator
Terminal window
pnpm docs:validate-frontmatter

Expected: zero errors.

  • Step 5: Rebuild Starlight
Terminal window
pnpm -F docs build

Expected: build passes (frontmatter extensions don’t break Starlight schema — Starlight ignores unknown keys).

  • Step 6: Commit
Terminal window
git add docs
git commit -m "docs: backfill frontmatter (quadrant, audience, last_reviewed) on all docs"

Task 2.4: Expose ?format=md raw-source routes

Section titled “Task 2.4: Expose ?format=md raw-source routes”

Starlight does not ship this natively as of 2026-04; implement a thin Astro endpoint.

Files:

  • Create: apps/docs/src/pages/[...slug].md.ts

  • Step 1: Write endpoint

import type { APIRoute } from "astro";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
export const GET: APIRoute = async ({ params }) => {
const slug = params.slug ?? "index";
const candidates = [
`../../docs/${slug}.md`,
`../../docs/${slug}/index.md`,
];
for (const rel of candidates) {
const abs = join(import.meta.dirname ?? "", rel);
try {
const content = await readFile(abs, "utf8");
return new Response(content, {
status: 200,
headers: { "Content-Type": "text/markdown; charset=utf-8" },
});
} catch {}
}
return new Response(`# Not found\n\nNo MD source at slug: ${slug}`, {
status: 404,
headers: { "Content-Type": "text/markdown; charset=utf-8" },
});
};
  • Step 2: Verify route
Terminal window
pnpm -F docs dev &
sleep 3
curl -s http://localhost:4321/architecture.md | head -5
kill %1

Expected: raw MD of docs/architecture.md returned.

  • Step 3: Commit
Terminal window
git add apps/docs/src/pages
git commit -m "feat(docs): expose /[slug].md raw-source endpoint for agents"

Task 2.5: Phase 2 wrap — status + changelog

Section titled “Task 2.5: Phase 2 wrap — status + changelog”
  • Step 1: Update CHANGELOG

Under [Unreleased]Added:

- `/llms.txt` + `/llms-full.txt` at docs site root (starlight-llms-txt).
- Frontmatter schema (`title`, `quadrant`, `audience`, `last_reviewed`) validated by `pnpm docs:validate-frontmatter`.
- `/<slug>.md` raw-source endpoint for agent consumption.
  • Step 2: Update docs/status.md

Move “Docs framework — Phase 2” from “In flight” to “Recently shipped”; add Phase 3 entry to “In flight”.

  • Step 3: Commit
Terminal window
git add CHANGELOG.md docs/status.md
git commit -m "docs: Phase 2 (agent layer) shipped"

Phase 3 — Auto-gen pipelines (day 2–3)

Section titled “Phase 3 — Auto-gen pipelines (day 2–3)”

Goal: docs/_generated/api/openapi.md, docs/_generated/db/schema.md, docs/_generated/db/erd.mmd, docs/_generated/config/env.md, docs/_generated/cli/pnpm-scripts.md all produced by pnpm docs:gen, regenerated before every build, gitignored.

Files:

  • Create: scripts/docs-gen-openapi.mjs

  • Modify: root package.json

  • Step 1: Verify existing OpenAPI spec generation works

Terminal window
pnpm -F api generate:openapi
test -f apps/api/openapi.json && echo "OK"

Expected: file exists with JSON.

  • Step 2: Install widdershins
Terminal window
pnpm add -w -D widdershins@<latest-stable>
  • Step 3: Write generator script

scripts/docs-gen-openapi.mjs:

import { mkdir, writeFile, readFile, copyFile } from "node:fs/promises";
import { execSync } from "node:child_process";
const OUT_DIR = "docs/_generated/api";
const SPEC = "apps/api/openapi.json";
await mkdir(OUT_DIR, { recursive: true });
// 1) regenerate OpenAPI spec from live Nest code
execSync("pnpm -F api generate:openapi", { stdio: "inherit" });
// 2) spec → MD via widdershins
execSync(
`pnpm exec widdershins ${SPEC} -o ${OUT_DIR}/openapi.md --language_tabs 'javascript:JavaScript' 'shell:cURL' --summary`,
{ stdio: "inherit" }
);
// 3) copy spec next to MD for Scalar viewer + SDK regen reference
await copyFile(SPEC, `${OUT_DIR}/openapi.json`);
// 4) prepend frontmatter so Starlight accepts the file
const md = await readFile(`${OUT_DIR}/openapi.md`, "utf8");
const frontmatter = `---
title: REST API reference
quadrant: reference
audience: dev
last_reviewed: ${new Date().toISOString().slice(0, 10)}
---
> **Auto-generated** from \`apps/api\` NestJS Swagger decorators. Do not edit by hand.
`;
await writeFile(`${OUT_DIR}/openapi.md`, frontmatter + md);
console.log(`✓ wrote ${OUT_DIR}/openapi.md`);
  • Step 4: Add script to root package.json
{
"scripts": {
"docs:gen:openapi": "node scripts/docs-gen-openapi.mjs"
}
}
  • Step 5: Run + verify
Terminal window
pnpm docs:gen:openapi
test -f docs/_generated/api/openapi.md && echo "OK"
grep -c "^##" docs/_generated/api/openapi.md # expect ≥ 30 route headers
  • Step 6: Commit
Terminal window
git add scripts/docs-gen-openapi.mjs package.json pnpm-lock.yaml
git commit -m "feat(docs): OpenAPI → Markdown auto-gen pipeline (30 routes)"

Task 3.2: Prisma schema + ERD → Markdown

Section titled “Task 3.2: Prisma schema + ERD → Markdown”

Files:

  • Create: scripts/docs-gen-prisma.mjs

  • Modify: apps/api/prisma/schema.prisma (add generator blocks)

  • Modify: root package.json

  • Step 1: Install Prisma doc generators

Terminal window
pnpm -F api add -D prisma-markdown@<latest-stable> prisma-erd-generator@<latest-stable>
  • Step 2: Add generator blocks to apps/api/prisma/schema.prisma
generator markdown {
provider = "prisma-markdown"
output = "../../../docs/_generated/db/schema.md"
title = "Ideony database schema"
}
generator erd {
provider = "prisma-erd-generator"
output = "../../../docs/_generated/db/erd.md"
theme = "default"
}
  • Step 3: Write wrapper script

scripts/docs-gen-prisma.mjs:

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { execSync } from "node:child_process";
await mkdir("docs/_generated/db", { recursive: true });
execSync("pnpm -F api exec prisma generate", { stdio: "inherit" });
// prepend frontmatter to both outputs
for (const name of ["schema.md", "erd.md"]) {
const path = `docs/_generated/db/${name}`;
const md = await readFile(path, "utf8");
const title = name === "schema.md" ? "Database schema" : "Entity relationship diagram";
const fm = `---
title: ${title}
quadrant: reference
audience: dev
last_reviewed: ${new Date().toISOString().slice(0, 10)}
---
> **Auto-generated** from \`apps/api/prisma/schema.prisma\`. Do not edit.
`;
if (!md.startsWith("---")) await writeFile(path, fm + md);
}
console.log("✓ wrote docs/_generated/db/{schema,erd}.md");
  • Step 4: Add root script
{
"scripts": {
"docs:gen:prisma": "node scripts/docs-gen-prisma.mjs"
}
}
  • Step 5: Run + verify
Terminal window
pnpm docs:gen:prisma
test -f docs/_generated/db/schema.md && echo "OK schema"
test -f docs/_generated/db/erd.md && echo "OK erd"
  • Step 6: Commit
Terminal window
git add scripts/docs-gen-prisma.mjs apps/api/prisma/schema.prisma package.json pnpm-lock.yaml apps/api/package.json
git commit -m "feat(docs): Prisma schema + ERD auto-gen pipeline"

Task 3.3: .env.example → env reference Markdown

Section titled “Task 3.3: .env.example → env reference Markdown”

Files:

  • Create: scripts/docs-gen-env.mjs

  • Modify: root package.json

  • Step 1: Write parser test first

scripts/docs-gen-env.test.mjs:

import { test } from "node:test";
import assert from "node:assert/strict";
import { parseEnvExample } from "./docs-gen-env.mjs";
test("parses KEY=value with comment above", () => {
const input = `# Database URL for Postgres connection
DATABASE_URL=postgresql://localhost:5432/ideony`;
const r = parseEnvExample(input);
assert.deepEqual(r, [
{
key: "DATABASE_URL",
description: "Database URL for Postgres connection",
example: "postgresql://localhost:5432/ideony",
},
]);
});
test("skips blank lines and bare comments", () => {
const input = `# Section header
# Redis host
REDIS_URL=redis://localhost:6379`;
const r = parseEnvExample(input);
assert.equal(r.length, 1);
assert.equal(r[0].key, "REDIS_URL");
});
test("handles empty values", () => {
const input = `# Optional Clerk secret
CLERK_SECRET_KEY=`;
const r = parseEnvExample(input);
assert.equal(r[0].example, "");
});

Run:

Terminal window
node --test scripts/docs-gen-env.test.mjs

Expected: fail (function undefined).

  • Step 2: Write docs-gen-env.mjs
import { readFile, mkdir, writeFile } from "node:fs/promises";
export function parseEnvExample(text) {
const lines = text.split("\n");
const out = [];
let lastComment = "";
for (const raw of lines) {
const line = raw.trim();
if (line.startsWith("#")) {
lastComment = line.replace(/^#\s*/, "").trim();
continue;
}
if (line === "") {
lastComment = "";
continue;
}
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
if (!m) continue;
out.push({ key: m[1], description: lastComment, example: m[2] });
lastComment = "";
}
return out;
}
if (import.meta.url === `file://${process.argv[1]}`) {
const src = await readFile(".env.example", "utf8");
const entries = parseEnvExample(src);
await mkdir("docs/_generated/config", { recursive: true });
const fm = `---
title: Environment variables
quadrant: reference
audience: dev
last_reviewed: ${new Date().toISOString().slice(0, 10)}
---
> **Auto-generated** from \`.env.example\`. Edit that file to change this table.
`;
const rows = entries
.map((e) => `| \`${e.key}\` | ${e.description || ""} | \`${e.example || ""}\` |`)
.join("\n");
const body = `| Variable | Description | Example |\n|----------|-------------|---------|\n${rows}\n`;
await writeFile("docs/_generated/config/env.md", fm + body);
console.log(`✓ wrote docs/_generated/config/env.md (${entries.length} vars)`);
}
  • Step 3: Run test, expect pass
Terminal window
node --test scripts/docs-gen-env.test.mjs
  • Step 4: Run generator + verify output
Terminal window
node scripts/docs-gen-env.mjs
head -10 docs/_generated/config/env.md
  • Step 5: Add root script
{
"scripts": {
"docs:gen:env": "node scripts/docs-gen-env.mjs"
}
}
  • Step 6: Commit
Terminal window
git add scripts/docs-gen-env.mjs scripts/docs-gen-env.test.mjs package.json
git commit -m "feat(docs): .env.example → Markdown reference (TDD, 3 tests)"

Files:

  • Create: scripts/docs-gen-cli.mjs

  • Create: docs/cli-descriptions.yml (hand-maintained)

  • Modify: root package.json

  • Step 1: Create docs/cli-descriptions.yml

# One-line descriptions for pnpm scripts surfaced in docs/_generated/cli/pnpm-scripts.md
# Keys match script name. Scripts without an entry are listed as "(no description)".
dev: Start all apps in dev mode (mobile + api)
build: Build all apps + packages
test: Run unit + integration tests (all workspaces)
lint: Biome lint + format check
lint:fix: Biome auto-fix lint + format
docker:up: Start Postgres + Redis + Mailpit + MinIO via docker-compose
docs:dev: Start Starlight dev server on http://localhost:4321
docs:build: Build Starlight static site via Turbo
docs:gen: Regenerate all auto-gen docs (OpenAPI, Prisma, env, CLI)
docs:gen:openapi: Regenerate docs/_generated/api/openapi.md from Nest Swagger
docs:gen:prisma: Regenerate docs/_generated/db/{schema,erd}.md from Prisma
docs:gen:env: Regenerate docs/_generated/config/env.md from .env.example
docs:gen:cli: Regenerate docs/_generated/cli/pnpm-scripts.md from package.json files
docs:validate-frontmatter: Validate YAML frontmatter schema on all docs
  • Step 2: Write generator script

scripts/docs-gen-cli.mjs:

import { readFile, readdir, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parse as parseYaml } from "yaml";
const root = process.cwd();
const descriptions = parseYaml(await readFile("docs/cli-descriptions.yml", "utf8")) ?? {};
async function collectScripts(pkgPath, workspaceName) {
try {
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
return Object.entries(pkg.scripts ?? {}).map(([k, v]) => ({
workspace: workspaceName,
name: k,
command: v,
description: descriptions[k] ?? "(no description)",
}));
} catch {
return [];
}
}
const all = [];
all.push(...(await collectScripts("package.json", "root")));
for (const subdir of ["apps", "packages"]) {
let entries;
try {
entries = await readdir(subdir, { withFileTypes: true });
} catch {
continue;
}
for (const ent of entries) {
if (!ent.isDirectory()) continue;
all.push(...(await collectScripts(join(subdir, ent.name, "package.json"), `${subdir}/${ent.name}`)));
}
}
await mkdir("docs/_generated/cli", { recursive: true });
const fm = `---
title: pnpm scripts reference
quadrant: reference
audience: dev
last_reviewed: ${new Date().toISOString().slice(0, 10)}
---
> **Auto-generated** from every \`package.json\`. Descriptions in \`docs/cli-descriptions.yml\`.
`;
const byWs = Object.groupBy(all, (s) => s.workspace);
let body = "";
for (const [ws, scripts] of Object.entries(byWs)) {
body += `\n## ${ws}\n\n| Script | Command | Description |\n|--------|---------|-------------|\n`;
for (const s of scripts) {
body += `| \`${s.name}\` | \`${s.command}\` | ${s.description} |\n`;
}
}
await writeFile("docs/_generated/cli/pnpm-scripts.md", fm + body);
console.log(`✓ wrote docs/_generated/cli/pnpm-scripts.md (${all.length} scripts)`);
  • Step 3: Add root script
{
"scripts": {
"docs:gen:cli": "node scripts/docs-gen-cli.mjs"
}
}
  • Step 4: Run + verify
Terminal window
pnpm docs:gen:cli
head -20 docs/_generated/cli/pnpm-scripts.md
  • Step 5: Commit
Terminal window
git add scripts/docs-gen-cli.mjs docs/cli-descriptions.yml package.json
git commit -m "feat(docs): pnpm scripts → Markdown reference"

Files:

  • Modify: root package.json

  • Step 1: Add aggregate script

{
"scripts": {
"docs:gen": "pnpm docs:gen:openapi && pnpm docs:gen:prisma && pnpm docs:gen:env && pnpm docs:gen:cli"
}
}
  • Step 2: Wire into Turbo docs:build dep (already done in Task 1.2)

Verify:

Terminal window
pnpm turbo run docs:build --dry-run=json | jq '.tasks[] | select(.taskId == "docs#docs:build") | .dependencies'

Expected: includes docs:gen.

  • Step 3: Full build passes
Terminal window
pnpm docs:gen && pnpm -F docs build

Expected: all _generated/**/*.md picked up by Starlight sidebar (autogenerate catches new files if config directories added to sidebar).

  • Step 4: Extend Starlight sidebar to surface _generated/

In apps/docs/astro.config.mjs, add to sidebar:

{
label: "API reference",
items: [
{ label: "REST routes", link: "/_generated/api/openapi" },
],
},
{
label: "Database",
items: [
{ label: "Schema", link: "/_generated/db/schema" },
{ label: "ERD", link: "/_generated/db/erd" },
],
},
{
label: "Config",
items: [
{ label: "Environment variables", link: "/_generated/config/env" },
{ label: "pnpm scripts", link: "/_generated/cli/pnpm-scripts" },
],
},
  • Step 5: Rebuild + smoke-check
Terminal window
pnpm docs:gen && pnpm -F docs build
pnpm -F docs preview &
sleep 2
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4322/_generated/api/openapi
kill %1

Expected: 200.

  • Step 6: Commit
Terminal window
git add package.json apps/docs/astro.config.mjs
git commit -m "feat(docs): aggregate docs:gen + surface _generated/ in Starlight sidebar"

  • Step 1: CHANGELOG + status

CHANGELOG.md under [Unreleased]Added:

- Auto-gen docs pipelines: OpenAPI → MD, Prisma schema+ERD → MD, `.env.example` → MD, pnpm scripts → MD.
- `docs/_generated/**` gitignored, regenerated via `pnpm docs:gen` before every Starlight build.

docs/status.md: move Phase 3 to “Recently shipped”, add Phase 4 to “In flight”.

  • Step 2: Commit
Terminal window
git add CHANGELOG.md docs/status.md
git commit -m "docs: Phase 3 (auto-gen pipelines) shipped"

Goal: markdownlint + vale + cspell + lychee + frontmatter validator + Mermaid syntax check + living-examples extraction all run in pre-commit (via Husky + lint-staged) and in CI.

Files:

  • Create: .markdownlint-cli2.jsonc

  • Modify: root package.json

  • Modify: .husky/pre-commit

  • Step 1: Install

Terminal window
pnpm add -w -D markdownlint-cli2@<latest-stable>
  • Step 2: Config

.markdownlint-cli2.jsonc:

{
"config": {
"default": true,
"MD013": false, // line length: off (prose wraps naturally)
"MD033": false, // inline HTML: allowed (MDX-ish)
"MD041": false, // first-line H1: off (frontmatter before H1)
"MD046": { "style": "fenced" }
},
"ignores": [
"node_modules/",
"**/node_modules/",
"apps/docs/dist/",
"docs/_generated/",
"docs/archive/**"
]
}
  • Step 3: Add script
{
"scripts": {
"docs:lint": "markdownlint-cli2 '**/*.md'"
}
}
  • Step 4: Run, fix errors
Terminal window
pnpm docs:lint

For each reported issue: fix the MD file minimally. If legitimate disagreement with a rule, add targeted disable comment <!-- markdownlint-disable-next-line MDxxx --> in that file.

  • Step 5: Wire into lint-staged

In root package.json lint-staged:

{
"lint-staged": {
"*.md": ["markdownlint-cli2 --fix"]
}
}

(merge with existing config)

  • Step 6: Commit
Terminal window
git add .markdownlint-cli2.jsonc package.json docs pnpm-lock.yaml
git commit -m "feat(docs): markdownlint gate + lint-staged pre-commit integration"

Files:

  • Create: .vale.ini

  • Create: .vale/styles/Ideony/BannedWords.yml

  • Create: .vale/styles/Ideony/PRLanguage.yml

  • Modify: root package.json

  • Step 1: Install vale (binary, not npm) — check existing

Terminal window
which vale || brew install vale
  • Step 2: Config

.vale.ini:

StylesPath = .vale/styles
MinAlertLevel = warning
[*.md]
BasedOnStyles = Vale, write-good, Ideony
  • Step 3: Custom “banned words” (from CLAUDE.md)

.vale/styles/Ideony/BannedWords.yml:

extends: existence
message: "'%s' is banned per CLAUDE.md — use plain language."
level: warning
ignorecase: true
tokens:
- critical
- crucial
- essential
- significant
- comprehensive
- robust
- elegant
- seamlessly
- blazingly
  • Step 4: Custom “PR language” check

.vale/styles/Ideony/PRLanguage.yml:

extends: existence
message: "Describe what the code does, not your feelings about it."
level: warning
ignorecase: true
tokens:
- I think
- I believe
- I feel
- unfortunately
- hopefully
  • Step 5: Sync external packages
Terminal window
vale sync
  • Step 6: Add script
{
"scripts": {
"docs:prose": "vale docs/ CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.md"
}
}
  • Step 7: Run + fix warnings
Terminal window
pnpm docs:prose

Fix occurrences in prose; allow warnings in code blocks (vale skips fenced code by default).

  • Step 8: Commit
Terminal window
git add .vale.ini .vale/styles/Ideony package.json docs CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.md
git commit -m "feat(docs): vale prose gate + Ideony banned-words style"

Files:

  • Create: cspell.json

  • Create: .cspell/ideony-dict.txt

  • Modify: root package.json

  • Step 1: Install

Terminal window
pnpm add -w -D cspell@<latest-stable>
  • Step 2: Config

cspell.json:

{
"version": "0.2",
"language": "en",
"dictionaryDefinitions": [
{
"name": "ideony",
"path": "./.cspell/ideony-dict.txt",
"addWords": true
}
],
"dictionaries": ["ideony"],
"ignorePaths": [
"node_modules/",
"**/node_modules/",
"apps/docs/dist/",
"docs/_generated/",
"docs/archive/**",
"**/*.lock",
"pnpm-lock.yaml"
]
}
  • Step 3: Seed ideony-dict.txt
Ideony
Dokploy
Clerk
Expo
NestJS
Fastify
Prisma
Mapbox
Stripe
Novu
BullMQ
Redis
Postgres
PostGIS
Gluestack
NativeWind
Pagefind
Starlight
Diátaxis
widdershins
lychee
markdownlint
  • Step 4: Add script
{
"scripts": {
"docs:spell": "cspell '**/*.md' '!**/node_modules/**' '!apps/docs/dist/**' '!docs/_generated/**'"
}
}
  • Step 5: Run + iterate
Terminal window
pnpm docs:spell

For each unknown word: add legitimate term to .cspell/ideony-dict.txt, fix genuine typos in MD.

  • Step 6: Commit
Terminal window
git add cspell.json .cspell package.json docs pnpm-lock.yaml
git commit -m "feat(docs): cspell gate + Ideony dictionary"

Files:

  • Create: lychee.toml

  • Modify: root package.json

  • Step 1: Install lychee

Terminal window
brew install lychee || cargo install lychee
  • Step 2: Config

lychee.toml:

# Skip network calls by default in pre-commit; CI opts in
include_verbatim = true
exclude_path = [
"node_modules/",
"apps/docs/dist/",
"docs/_generated/",
"docs/archive/",
]
exclude = [
"http://localhost",
"https://localhost",
"http://127.0.0.1",
"ideony.is-a.dev", # zone pending — remove after active
]
max_concurrency = 8
cache = true
max_cache_age = "2d"
accept = [200, 403, 429]
method = "GET"
  • Step 3: Add scripts (local = offline only; CI = online)
{
"scripts": {
"docs:links": "lychee --offline --no-progress docs/ CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.md",
"docs:links:ci": "lychee --no-progress docs/ CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.md"
}
}
  • Step 4: Run offline, fix internal dead links
Terminal window
pnpm docs:links

For each dead internal link: fix path or remove.

  • Step 5: Commit
Terminal window
git add lychee.toml package.json docs
git commit -m "feat(docs): lychee link-check gate (offline pre-commit, online CI)"

Files:

  • Create: scripts/docs-lint-mermaid.mjs

  • Modify: root package.json

  • Step 1: Install mermaid-cli

Terminal window
pnpm add -w -D @mermaid-js/mermaid-cli@<latest-stable>
  • Step 2: Write linter

scripts/docs-lint-mermaid.mjs:

import { readFile } from "node:fs/promises";
import { execSync } from "node:child_process";
import { globby } from "globby";
import { writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const files = await globby(["docs/**/*.md", "docs/**/*.mmd", "!docs/archive/**", "!docs/_generated/**"]);
let failed = 0;
for (const file of files) {
const content = await readFile(file, "utf8");
const blocks = file.endsWith(".mmd")
? [{ code: content, n: 0 }]
: [...content.matchAll(/```mermaid\n([\s\S]*?)\n```/g)].map((m, n) => ({ code: m[1], n }));
for (const b of blocks) {
const tmp = join(tmpdir(), `mermaid-${Date.now()}-${b.n}.mmd`);
const out = tmp.replace(".mmd", ".svg");
await writeFile(tmp, b.code);
try {
execSync(`pnpm exec mmdc -i ${tmp} -o ${out} -q`, { stdio: "pipe" });
} catch (e) {
console.error(`${file} block #${b.n} invalid:\n${e.stderr?.toString() ?? e.message}`);
failed++;
}
}
}
if (failed > 0) {
console.error(`\n${failed} Mermaid block(s) failed to render`);
process.exit(1);
}
console.log(`✓ all ${files.length} files had valid Mermaid`);
  • Step 3: Install globby
Terminal window
pnpm add -w -D globby@<latest-stable>
  • Step 4: Add script
{
"scripts": {
"docs:lint:mermaid": "node scripts/docs-lint-mermaid.mjs"
}
}
  • Step 5: Run
Terminal window
pnpm docs:lint:mermaid

Expected (before Phase 6 diagram migration): likely zero mermaid blocks — pass trivially. After Phase 6: catches broken diagrams.

  • Step 6: Commit
Terminal window
git add scripts/docs-lint-mermaid.mjs package.json pnpm-lock.yaml
git commit -m "feat(docs): Mermaid syntax gate"

Task 4.6: Living-examples extraction (local-only, CI block deferred to Phase 4.5)

Section titled “Task 4.6: Living-examples extraction (local-only, CI block deferred to Phase 4.5)”

Files:

  • Create: scripts/docs-extract-examples.mjs

  • Create: tsconfig.docs-examples.json

  • Modify: root package.json

  • Step 1: Write extractor

scripts/docs-extract-examples.mjs:

import { readFile, mkdir, writeFile, rm } from "node:fs/promises";
import { globby } from "globby";
import { createHash } from "node:crypto";
import { execSync } from "node:child_process";
const TMP = "tmp/docs-examples";
await rm(TMP, { recursive: true, force: true });
await mkdir(TMP, { recursive: true });
const files = await globby(["docs/**/*.md", "!docs/archive/**", "!docs/_generated/**"]);
let count = 0;
for (const file of files) {
const content = await readFile(file, "utf8");
const blocks = [...content.matchAll(/```ts example\n([\s\S]*?)\n```/g)];
for (const b of blocks) {
const hash = createHash("sha1").update(b[1]).digest("hex").slice(0, 8);
const name = file.replace(/[\/.]/g, "-") + "-" + hash + ".ts";
await writeFile(`${TMP}/${name}`, b[1]);
count++;
}
}
if (count === 0) {
console.log("✓ no `ts example` blocks found (nothing to check)");
process.exit(0);
}
console.log(`Extracted ${count} examples → ${TMP}`);
try {
execSync(`pnpm exec tsc --project tsconfig.docs-examples.json`, { stdio: "inherit" });
console.log(`✓ all ${count} examples compile`);
} catch {
console.error("✗ at least one example failed to compile");
// Phase 4.5 will turn this into `process.exit(1)`. For now, warn only.
console.warn("(Phase 4 local-only: not blocking; promoted to CI in Phase 4.5)");
}
  • Step 2: Create tsconfig.docs-examples.json
{
"extends": "./packages/tsconfig/base.json",
"compilerOptions": {
"noEmit": true,
"skipLibCheck": true
},
"include": ["tmp/docs-examples/**/*.ts"]
}
  • Step 3: Add script
{
"scripts": {
"docs:examples": "node scripts/docs-extract-examples.mjs"
}
}
  • Step 4: Run
Terminal window
pnpm docs:examples

Expected: “no blocks found” (Phase 4 baseline — no ts example tagged blocks yet).

  • Step 5: Commit
Terminal window
git add scripts/docs-extract-examples.mjs tsconfig.docs-examples.json package.json
git commit -m "feat(docs): living-examples extraction (local-only; CI gate deferred to Phase 4.5)"

Task 4.7: Aggregate docs:quality + wire pre-commit

Section titled “Task 4.7: Aggregate docs:quality + wire pre-commit”

Files:

  • Modify: root package.json

  • Modify: .husky/pre-commit

  • Step 1: Aggregate script

{
"scripts": {
"docs:quality": "pnpm docs:validate-frontmatter && pnpm docs:lint && pnpm docs:prose && pnpm docs:spell && pnpm docs:links && pnpm docs:lint:mermaid"
}
}
  • Step 2: Wire into pre-commit

Append to .husky/pre-commit:

Terminal window
# Docs quality gates — fail fast on staged MD files
STAGED_MD=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)
if [ -n "$STAGED_MD" ]; then
pnpm docs:validate-frontmatter
pnpm docs:lint
fi

(Heavier gates like vale / cspell / lychee / mermaid stay in CI + pnpm docs:quality — avoid slowing every commit.)

  • Step 3: Verify

Stage a bad MD, attempt commit, expect fail. Fix, commit succeeds.

  • Step 4: Commit
Terminal window
git add package.json .husky/pre-commit
git commit -m "feat(docs): docs:quality aggregate + pre-commit integration"

Files:

  • Create: .github/workflows/docs-quality.yml

  • Step 1: Write workflow

name: docs-quality
on:
pull_request:
paths:
- "docs/**"
- "apps/docs/**"
- "CHANGELOG.md"
- "CLAUDE.md"
- "README.md"
- "CONTRIBUTING.md"
- ".markdownlint-cli2.jsonc"
- ".vale.ini"
- ".vale/**"
- "cspell.json"
- "lychee.toml"
- "scripts/docs-*"
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm docs:gen
- run: pnpm docs:validate-frontmatter
- run: pnpm docs:lint
- uses: errata-ai/vale-action@reviewdog
with:
files: "docs/ CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.md"
- run: pnpm docs:spell
- uses: lycheeverse/lychee-action@v2
with:
args: "--config lychee.toml docs/ CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.md"
- run: pnpm docs:lint:mermaid
  • Step 2: Commit
Terminal window
git add .github/workflows/docs-quality.yml
git commit -m "feat(ci): docs-quality workflow"
  • Step 3: Note

Cloud CI disabled per reminder_cloud_cicd_disabled — workflow sits unused until billing resolves. Local gate still enforces on every commit.


Files:

  • Create: .github/workflows/docs-deploy.yml

  • Step 1: Write

name: docs-deploy
on:
push:
branches: [main]
paths:
- "docs/**"
- "apps/docs/**"
- ".github/workflows/docs-deploy.yml"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm docs:gen
- run: pnpm -F docs build
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy apps/docs/dist --project-name=ideony-docs --branch=main
- name: Smoke check
run: |
sleep 10
curl -sSf -o /dev/null https://docs.ideony.is-a.dev/llms.txt
curl -sSf -o /dev/null https://docs.ideony.is-a.dev/architecture
  • Step 2: Add secrets

Via GH UI: CLOUDFLARE_API_TOKEN (Pages:Edit scope), CLOUDFLARE_ACCOUNT_ID.

  • Step 3: Commit
Terminal window
git add .github/workflows/docs-deploy.yml
git commit -m "feat(ci): docs-deploy workflow (Cloudflare Pages + smoke)"

  • Step 1: CHANGELOG + status

CHANGELOG [Unreleased]Added:

- Quality gates (local + CI): markdownlint, vale (+ Ideony banned-words style), cspell (+ Ideony dict), lychee, frontmatter validator, Mermaid syntax check.
- Living-examples extraction (local-only; CI block deferred to Phase 4.5).
- `docs-quality.yml` + `docs-deploy.yml` GitHub Actions workflows.

docs/status.md: Phase 4 → shipped; Phase 5 + 6 → in flight (parallel).

  • Step 2: Commit
Terminal window
git add CHANGELOG.md docs/status.md
git commit -m "docs: Phase 4 (quality gates) shipped"

Phase 5 — Zero-drift audit + upgrade (day 3–4, parallel with Phase 6)

Section titled “Phase 5 — Zero-drift audit + upgrade (day 3–4, parallel with Phase 6)”

Goal: every row in the CLAUDE.md trigger matrix is gate-enforced by scripts/check-docs-updated.sh, not honor-system.

Task 5.1: Read current gate + matrix side-by-side

Section titled “Task 5.1: Read current gate + matrix side-by-side”

Files:

  • Read-only: scripts/check-docs-updated.sh, scripts/check-docs-updated.test.sh, scripts/README-docs-gate.md, CLAUDE.md

  • Step 1: Read every source file

Summarize what check-docs-updated.sh currently enforces. Make a table.

  • Step 2: Read the CLAUDE.md trigger matrix (14 rows)

Copy the matrix into a new file docs/research/2026-04-2x-docs-gate-audit.md (date = today). Add columns: Already enforced? (Y/N), Gap severity (high/med/low), Proposed fix.

  • Step 3: Walk each row, fill in columns

Examples of expected gaps:

  • “Shipped a feature” row → matrix says update CHANGELOG + status.md + roadmap.md. Current gate likely only requires ONE doc touched.

  • “Made an architectural decision” row → matrix says new ADR. Current gate may not detect “this is an arch decision” automatically.

  • Step 4: Commit audit report

Terminal window
git add docs/research/2026-04-2x-docs-gate-audit.md
git commit -m "docs(research): zero-drift gate vs CLAUDE.md trigger matrix audit"

Task 5.2: Upgrade gate — shipped-feature detection

Section titled “Task 5.2: Upgrade gate — shipped-feature detection”

Files:

  • Modify: scripts/check-docs-updated.sh

  • Modify: scripts/check-docs-updated.test.sh

  • Step 1: Write failing test

scripts/check-docs-updated.test.sh:

Terminal window
# Test: feat: commit type MUST stage CHANGELOG + status.md + roadmap.md
test_feat_requires_three_docs() {
setup_repo
stage_file "apps/mobile/src/new-feature.tsx" "// feature"
stage_commit_msg "feat(mobile): add new feature"
run_check && fail "expected gate to block feat without CHANGELOG + status + roadmap"
pass
}

Run: expect fail.

  • Step 2: Add rule to gate

In scripts/check-docs-updated.sh, add logic:

Terminal window
# If commit message starts with `feat:` or `feat(`, require all three:
if echo "$COMMIT_MSG" | grep -qE '^feat(\(|:)'; then
REQUIRED="CHANGELOG.md docs/status.md docs/roadmap.md"
for doc in $REQUIRED; do
if ! echo "$STAGED" | grep -q "^$doc$"; then
echo "✗ feat: commit requires $doc to be staged (trigger matrix row: Shipped a feature)"
FAIL=1
fi
done
fi
  • Step 3: Test passes

  • Step 4: Commit

Terminal window
git add scripts/check-docs-updated.sh scripts/check-docs-updated.test.sh
git commit -m "feat(docs-gate): feat: commits require CHANGELOG + status + roadmap"

For each gap identified in Task 5.1, repeat the TDD cycle:

  • Step 1-N: one task per gap row

Each gap: write failing test → add rule → pass → commit. Keep commits granular (one rule per commit).


  • Step 1: CHANGELOG + status

CHANGELOG: note gate upgrades. status.md: Phase 5 → shipped.

  • Step 2: Commit
Terminal window
git add CHANGELOG.md docs/status.md
git commit -m "docs: Phase 5 (zero-drift gate upgrade) shipped"

Phase 6 — Diagram migration (day 4, parallel with Phase 5)

Section titled “Phase 6 — Diagram migration (day 4, parallel with Phase 5)”

Goal: all architecture + flow diagrams in docs/ are Mermaid source (renderable, diffable); no PNG / JPG.

Task 6.1: Install rehype-mermaid in Starlight

Section titled “Task 6.1: Install rehype-mermaid in Starlight”

Files:

  • Modify: apps/docs/package.json

  • Modify: apps/docs/astro.config.mjs

  • Step 1: Install

Terminal window
pnpm -F docs add rehype-mermaid@<latest-stable> playwright@<latest-stable>
pnpm -F docs exec playwright install chromium
  • Step 2: Wire into Astro
import rehypeMermaid from "rehype-mermaid";
export default defineConfig({
markdown: {
rehypePlugins: [[rehypeMermaid, { strategy: "img-png" }]],
},
integrations: [starlight({ /* ... */ })],
});
  • Step 3: Add test mermaid block

In docs/architecture.md, add a sample mermaid block (or new H2 section). Build + verify it renders.

  • Step 4: Commit
Terminal window
git add apps/docs/package.json apps/docs/astro.config.mjs pnpm-lock.yaml
git commit -m "feat(docs): rehype-mermaid for inline diagram rendering"

Task 6.2: Migrate docs/architecture.md C4 blocks

Section titled “Task 6.2: Migrate docs/architecture.md C4 blocks”

Files:

  • Modify: docs/architecture.md

  • Step 1: Identify existing diagrams

Read docs/architecture.md, list ASCII / code-block architecture visuals.

  • Step 2: Rewrite each as mermaid block

Example C4 Context:

```mermaid
C4Context
title Ideony — system context
Person(consumer, "Consumer", "Books services")
Person(pro, "Professional", "Accepts jobs")
System(ideony, "Ideony", "Marketplace + SOS dispatch")
System_Ext(clerk, "Clerk", "Auth")
System_Ext(stripe, "Stripe", "Payments")
System_Ext(mapbox, "Mapbox", "Maps + routing")
Rel(consumer, ideony, "Browses, books, chats")
Rel(pro, ideony, "Receives, accepts, completes")
Rel(ideony, clerk, "JWT verify + webhooks")
Rel(ideony, stripe, "Connect + PaymentIntents")
Rel(ideony, mapbox, "Geocoding + directions")
```

Repeat for sequence diagrams, container diagrams, etc.

  • Step 3: Verify render
Terminal window
pnpm -F docs build
  • Step 4: Commit
Terminal window
git add docs/architecture.md
git commit -m "docs(architecture): migrate C4 blocks to Mermaid diagrams"

Task 6.3: Audit repo for stray PNG / JPG in docs

Section titled “Task 6.3: Audit repo for stray PNG / JPG in docs”
  • Step 1: Grep
Terminal window
find docs -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \) | grep -v archive
  • Step 2: For each hit

  • If it’s a logo / screenshot of a real UI → keep (logos acceptable; real screenshots acceptable when documenting UI state).

  • If it’s a diagram that should be code → rewrite as .mmd / inline mermaid, delete image.

  • Step 3: Commit per migrated diagram


  • Step 1: CHANGELOG + status
- Architecture diagrams now Mermaid source (no PNG diagrams in docs/).

docs/status.md: Phase 6 → shipped.

  • Step 2: Commit
Terminal window
git add CHANGELOG.md docs/status.md
git commit -m "docs: Phase 6 (diagram migration) shipped"

Phase 7 — Cofounder migration (day 5+, async, non-blocking)

Section titled “Phase 7 — Cofounder migration (day 5+, async, non-blocking)”

Goal: cofounder-facing content from the Google Drive doc lives in docs/ tagged audience: cofounder, Drive stays in parallel (no hard sunset).

Files:

  • Create: docs/cofounder-overview.md

  • Modify: individual docs/specs/*.md where Drive content maps to existing specs

  • Step 1: Read Drive doc (URL in CLAUDE.md)

https://docs.google.com/document/d/1tyv9Lyad3nZzeM_tLFsLdmGmS1WL7PexOMtJlTBmiuw/edit

List sections.

  • Step 2: Map each section to target MD location

Product vision → docs/cofounder-overview.md Business model → docs/cofounder-overview.md Locked decisions → existing ADRs (cross-reference; no duplication) Open questions → new docs/specs/cofounder-open-questions.md

  • Step 3: Write docs/cofounder-overview.md

Frontmatter:

---
title: Cofounder overview
quadrant: explanation
audience: cofounder
last_reviewed: 2026-04-21
---

Body = prose-rewritten Drive content; no tech jargon; link to ADRs for detail.

  • Step 4: Commit per file

Task 7.2: Verify Starlight renders audience: cofounder pages correctly

Section titled “Task 7.2: Verify Starlight renders audience: cofounder pages correctly”
  • Step 1: Build + preview
Terminal window
pnpm -F docs build && pnpm -F docs preview
  • Step 2: Auth as a cofounder email (test)

Access docs.ideony.is-a.dev/cofounder-overview, verify page renders + is readable.

  • Step 3: Commit

Phase 4.5 — Living-examples CI block (day 5+, gated on Phase 4 clean ≥ 1 week)

Section titled “Phase 4.5 — Living-examples CI block (day 5+, gated on Phase 4 clean ≥ 1 week)”

Goal: promote living-examples from local-only warning to CI fail.

Files:

  • Modify: scripts/docs-extract-examples.mjs

  • Step 1: Change the warn branch to fail

console.warn("(Phase 4 local-only: not blocking; promoted to CI in Phase 4.5)");
process.exit(1);
  • Step 2: Add step to docs-quality.yml
- run: pnpm docs:examples
  • Step 3: Commit
Terminal window
git add scripts/docs-extract-examples.mjs .github/workflows/docs-quality.yml
git commit -m "feat(docs): promote living-examples to CI blocker (Phase 4.5)"

Phase 8 — Federation prep (deferred until Jooice docs init)

Section titled “Phase 8 — Federation prep (deferred until Jooice docs init)”

Goal: extract shared Starlight config to packages/docs-shell/ for reuse across Ideony + Jooice.

Task 8.1: Extract shared config (outline only — defer impl)

Section titled “Task 8.1: Extract shared config (outline only — defer impl)”

Files (planned):

  • Create: packages/docs-shell/package.json
  • Create: packages/docs-shell/src/astro-preset.ts
  • Create: packages/docs-shell/src/vale-styles/**
  • Create: packages/docs-shell/src/cspell-dict.txt
  • Modify: apps/docs/astro.config.mjs to import preset

Do not start until Jooice has a matching apps/docs/ to share with. Adds speculative complexity before.


Spec coverage:

Spec sectionTasks
§1 goalscovered by cumulative phases
§2 framework pick§1 install + config (Tasks 1.1–1.3)
§3 repo layoutTask 1.1 (apps/docs/ scaffold), Task 1.3 (gitignore _generated/)
§4 auto-genTasks 3.1–3.5
§5 quality gatesTasks 4.1–4.10
§6 zero-drift auditTasks 5.1–5.4
§7 agent layerTasks 2.1–2.4
§8 diagramsTasks 6.1–6.4
§9 cofounder accessTask 1.5 (Zero Trust), Task 7.1 (content migration)
§10 versioningOut of scope pre-launch (spec §10 self-declares inactive)
§11 federationPhase 8 outline
§12 CI + hostingTasks 4.8 (quality workflow), 4.9 (deploy workflow), 1.5 (CF Pages)
§13 rollout phasesMatched 1:1 to Phase 1–8 below
§14 risksNo task — risks documented in spec §14; mitigations built into tasks (worktree, gitignore, pre-commit)
§15 out of scopeHonored (no Kapa.ai, no public subdomain, no i18n, no Storybook)

Placeholder scan: one placeholder intentional — <PIN-to-latest-stable> in Task 1.1 (replaced via Context7 lookup in Step 1 of same task). No TBD / TODO / “implement later” in task bodies.

Type consistency: no types defined across tasks (scaffolding-heavy plan). Script function names (validateDoc, parseEnvExample) used consistently where referenced.

Spec gaps: none — every locked goal (§1, #1–#10) maps to one or more tasks.


  • Worktree-strict: dispatch via git worktree per CLAUDE.md agent policy. Worktree path = .claude/worktrees/agent-docs-framework/.
  • Parallel-safety vs running E2E agents: collision surface = pnpm-lock.yaml + root package.json. Accept rebase cost. Avoid Task 1.1 Step 9 (pnpm install) during peak E2E agent activity.
  • Commit cadence: one commit per task minimum; more granular inside multi-step tasks where natural.
  • Rollback: every phase is reversible. Revert any single phase’s commits without breaking earlier phases.