Plan: Docs framework — Starlight + llms.txt + auto-gen implementation
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto 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.
Architecture
Section titled “Architecture”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.
Tech Stack
Section titled “Tech Stack”- 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 ordering
Section titled “Phase ordering”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-blockingPhase 4.5 (living-examples CI) — gated on Phase 4 running clean ≥ 1 weekPhase 8 (federation prep) — deferred until Jooice docs initCommit frequently (one per task where possible). Each phase ends with a status.md update + CHANGELOG under [Unreleased].
Phase 1 — Renderer shell (day 1)
Section titled “Phase 1 — Renderer shell (day 1)”Goal: apps/docs/ Astro-Starlight app live locally, content-loading docs/**/*.md, deployed to Cloudflare Pages behind Zero Trust.
Task 1.1: Scaffold apps/docs/ workspace
Section titled “Task 1.1: Scaffold apps/docs/ workspace”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 globsapps/*, 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-checkimport { 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.astronode_modules.env- Step 7: Create placeholder logo + custom CSS
mkdir -p apps/docs/src/assets apps/docs/src/stylesapps/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
```bashpnpm -F docs dev # local preview on http://localhost:4321pnpm -F docs build # static build → dist/pnpm -F docs preview # preview built siteDeploy
Section titled “Deploy”Cloudflare Pages project ideony-docs → docs.ideony.is-a.dev. See docs/infrastructure.md.
Related
Section titled “Related”- Spec:
docs/specs/2026-04-21-docs-framework-design.md - ADR:
docs/decisions/0027-docs-framework-starlight.md
- [ ] **Step 9: Install + verify local dev**
Run:```bashpnpm installpnpm -F docs devExpected: Astro dev server starts on http://localhost:4321, landing page renders.
- Step 10: Check build passes
Run:
pnpm -F docs buildExpected: dist/ folder produced with static HTML, no errors.
- Step 11: Commit
git add apps/docs pnpm-workspace.yaml pnpm-lock.yamlgit commit -m "feat(docs): scaffold apps/docs Starlight renderer"Docs gate note: touches apps/docs/ (new); CLAUDE.md update happens in Task 1.6.
Task 1.2: Add Turborepo pipeline entries
Section titled “Task 1.2: Add Turborepo pipeline entries”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:genanddocs:buildtasks
{ "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:
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
git add turbo.json package.jsongit commit -m "chore(docs): wire docs:gen + docs:build into turbo pipeline"Task 1.3: Gitignore docs/_generated/
Section titled “Task 1.3: Gitignore docs/_generated/”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
git add .gitignoregit 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:
pnpm -F docs build 2>&1 | tee /tmp/starlight-build.log- Step 2: Grep for errors
Run:
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<or wrap in code fence.
Do NOT change content; only minimally unblock rendering.
- Step 4: Rerun build to green
pnpm -F docs buildExpected: zero errors.
- Step 5: Commit
git add docsgit 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, typeself-hosted, domaindocs.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
git add docs/infrastructure.mdgit 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
git add CLAUDE.md docs/README.md CHANGELOG.md docs/status.mdgit commit -m "docs: reference rendered Starlight site across top-level docs"Phase 2 — Agent layer (day 1–2)
Section titled “Phase 2 — Agent layer (day 1–2)”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-txtversion via Context7 or npm
Run:
npm view starlight-llms-txt versionPin to stable (not next / alpha).
- Step 2: Add dep
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
pnpm -F docs buildtest -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.txtsanity
head -20 apps/docs/dist/llms.txtwc -l apps/docs/dist/llms-full.txtExpected: llms.txt has project header + section links; llms-full.txt > 1000 lines.
- Step 6: Commit
git add apps/docs/package.json apps/docs/astro.config.mjs pnpm-lock.yamlgit 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: Architecturequadrant: referenceaudience: devlast_reviewed: 2026-04-21---# Architecture`; const r = validateDoc("docs/architecture.md", md); assert.equal(r.ok, true);});
test("missing quadrant fails", () => { const md = `---title: Fooaudience: 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: Fooquadrant: nonsenseaudience: devlast_reviewed: 2026-04-21---# Foo`; const r = validateDoc("docs/foo.md", md); assert.equal(r.ok, false);});
test("invalid audience fails", () => { const md = `---title: Fooquadrant: referenceaudience: marketinglast_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
node --test scripts/validate-frontmatter.test.mjsExpected: 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 argsif (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
yamldep at root
pnpm add -w -D yaml@^2.5.0- Step 5: Run test, expect pass
node --test scripts/validate-frontmatter.test.mjsExpected: 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
git add scripts/validate-frontmatter.mjs scripts/validate-frontmatter.test.mjs package.json pnpm-lock.yamlgit 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
pnpm docs:validate-frontmatter || trueCapture all error messages.
- Step 2: Map each file to quadrant + audience
Reference docs/README.md quadrant table. Defaults:
-
docs/getting-started.md→quadrant: tutorial,audience: dev -
docs/architecture.md→quadrant: mixed,audience: dev -
docs/design-system.md→quadrant: reference,audience: dev -
docs/infrastructure.md→quadrant: mixed,audience: dev -
docs/testing.md→quadrant: mixed,audience: dev -
docs/roadmap.md→quadrant: explanation,audience: cofounder -
docs/status.md→quadrant: reference,audience: dev -
docs/glossary.md→quadrant: reference,audience: dev -
docs/README.md→quadrant: explanation,audience: dev -
docs/decisions/*.md→quadrant: explanation,audience: dev -
docs/specs/*.md→quadrant: explanation,audience: dev -
docs/plans/*.md→quadrant: how-to,audience: dev -
docs/research/*.md→quadrant: reference,audience: dev -
Step 3: For each file, add frontmatter block at top
Example for docs/architecture.md:
---title: Architecturequadrant: mixedaudience: devlast_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
pnpm docs:validate-frontmatterExpected: zero errors.
- Step 5: Rebuild Starlight
pnpm -F docs buildExpected: build passes (frontmatter extensions don’t break Starlight schema — Starlight ignores unknown keys).
- Step 6: Commit
git add docsgit 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
pnpm -F docs dev &sleep 3curl -s http://localhost:4321/architecture.md | head -5kill %1Expected: raw MD of docs/architecture.md returned.
- Step 3: Commit
git add apps/docs/src/pagesgit 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
git add CHANGELOG.md docs/status.mdgit 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.
Task 3.1: OpenAPI → Markdown pipeline
Section titled “Task 3.1: OpenAPI → Markdown pipeline”Files:
-
Create:
scripts/docs-gen-openapi.mjs -
Modify: root
package.json -
Step 1: Verify existing OpenAPI spec generation works
pnpm -F api generate:openapitest -f apps/api/openapi.json && echo "OK"Expected: file exists with JSON.
- Step 2: Install widdershins
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 codeexecSync("pnpm -F api generate:openapi", { stdio: "inherit" });
// 2) spec → MD via widdershinsexecSync( `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 referenceawait copyFile(SPEC, `${OUT_DIR}/openapi.json`);
// 4) prepend frontmatter so Starlight accepts the fileconst md = await readFile(`${OUT_DIR}/openapi.md`, "utf8");const frontmatter = `---title: REST API referencequadrant: referenceaudience: devlast_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
pnpm docs:gen:openapitest -f docs/_generated/api/openapi.md && echo "OK"grep -c "^##" docs/_generated/api/openapi.md # expect ≥ 30 route headers- Step 6: Commit
git add scripts/docs-gen-openapi.mjs package.json pnpm-lock.yamlgit 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
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 outputsfor (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: referenceaudience: devlast_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
pnpm docs:gen:prismatest -f docs/_generated/db/schema.md && echo "OK schema"test -f docs/_generated/db/erd.md && echo "OK erd"- Step 6: Commit
git add scripts/docs-gen-prisma.mjs apps/api/prisma/schema.prisma package.json pnpm-lock.yaml apps/api/package.jsongit 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 connectionDATABASE_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 hostREDIS_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 secretCLERK_SECRET_KEY=`; const r = parseEnvExample(input); assert.equal(r[0].example, "");});Run:
node --test scripts/docs-gen-env.test.mjsExpected: 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 variablesquadrant: referenceaudience: devlast_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
node --test scripts/docs-gen-env.test.mjs- Step 4: Run generator + verify output
node scripts/docs-gen-env.mjshead -10 docs/_generated/config/env.md- Step 5: Add root script
{ "scripts": { "docs:gen:env": "node scripts/docs-gen-env.mjs" }}- Step 6: Commit
git add scripts/docs-gen-env.mjs scripts/docs-gen-env.test.mjs package.jsongit commit -m "feat(docs): .env.example → Markdown reference (TDD, 3 tests)"Task 3.4: pnpm scripts → Markdown
Section titled “Task 3.4: pnpm scripts → Markdown”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 + packagestest: Run unit + integration tests (all workspaces)lint: Biome lint + format checklint:fix: Biome auto-fix lint + formatdocker:up: Start Postgres + Redis + Mailpit + MinIO via docker-composedocs:dev: Start Starlight dev server on http://localhost:4321docs:build: Build Starlight static site via Turbodocs:gen: Regenerate all auto-gen docs (OpenAPI, Prisma, env, CLI)docs:gen:openapi: Regenerate docs/_generated/api/openapi.md from Nest Swaggerdocs:gen:prisma: Regenerate docs/_generated/db/{schema,erd}.md from Prismadocs:gen:env: Regenerate docs/_generated/config/env.md from .env.exampledocs:gen:cli: Regenerate docs/_generated/cli/pnpm-scripts.md from package.json filesdocs: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 referencequadrant: referenceaudience: devlast_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
pnpm docs:gen:clihead -20 docs/_generated/cli/pnpm-scripts.md- Step 5: Commit
git add scripts/docs-gen-cli.mjs docs/cli-descriptions.yml package.jsongit commit -m "feat(docs): pnpm scripts → Markdown reference"Task 3.5: Aggregate docs:gen
Section titled “Task 3.5: Aggregate docs:gen”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:builddep (already done in Task 1.2)
Verify:
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
pnpm docs:gen && pnpm -F docs buildExpected: 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
pnpm docs:gen && pnpm -F docs buildpnpm -F docs preview &sleep 2curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4322/_generated/api/openapikill %1Expected: 200.
- Step 6: Commit
git add package.json apps/docs/astro.config.mjsgit commit -m "feat(docs): aggregate docs:gen + surface _generated/ in Starlight sidebar"Task 3.6: Phase 3 wrap
Section titled “Task 3.6: Phase 3 wrap”- 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
git add CHANGELOG.md docs/status.mdgit commit -m "docs: Phase 3 (auto-gen pipelines) shipped"Phase 4 — Quality gates local (day 3)
Section titled “Phase 4 — Quality gates local (day 3)”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.
Task 4.1: markdownlint
Section titled “Task 4.1: markdownlint”Files:
-
Create:
.markdownlint-cli2.jsonc -
Modify: root
package.json -
Modify:
.husky/pre-commit -
Step 1: Install
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
pnpm docs:lintFor 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
git add .markdownlint-cli2.jsonc package.json docs pnpm-lock.yamlgit commit -m "feat(docs): markdownlint gate + lint-staged pre-commit integration"Task 4.2: vale prose style
Section titled “Task 4.2: vale prose style”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
which vale || brew install vale- Step 2: Config
.vale.ini:
StylesPath = .vale/stylesMinAlertLevel = warning
[*.md]BasedOnStyles = Vale, write-good, Ideony- Step 3: Custom “banned words” (from CLAUDE.md)
.vale/styles/Ideony/BannedWords.yml:
extends: existencemessage: "'%s' is banned per CLAUDE.md — use plain language."level: warningignorecase: truetokens: - critical - crucial - essential - significant - comprehensive - robust - elegant - seamlessly - blazingly- Step 4: Custom “PR language” check
.vale/styles/Ideony/PRLanguage.yml:
extends: existencemessage: "Describe what the code does, not your feelings about it."level: warningignorecase: truetokens: - I think - I believe - I feel - unfortunately - hopefully- Step 5: Sync external packages
vale sync- Step 6: Add script
{ "scripts": { "docs:prose": "vale docs/ CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.md" }}- Step 7: Run + fix warnings
pnpm docs:proseFix occurrences in prose; allow warnings in code blocks (vale skips fenced code by default).
- Step 8: Commit
git add .vale.ini .vale/styles/Ideony package.json docs CHANGELOG.md CLAUDE.md README.md CONTRIBUTING.mdgit commit -m "feat(docs): vale prose gate + Ideony banned-words style"Task 4.3: cspell spell-check
Section titled “Task 4.3: cspell spell-check”Files:
-
Create:
cspell.json -
Create:
.cspell/ideony-dict.txt -
Modify: root
package.json -
Step 1: Install
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
IdeonyDokployClerkExpoNestJSFastifyPrismaMapboxStripeNovuBullMQRedisPostgresPostGISGluestackNativeWindPagefindStarlightDiátaxiswiddershinslycheemarkdownlint- Step 4: Add script
{ "scripts": { "docs:spell": "cspell '**/*.md' '!**/node_modules/**' '!apps/docs/dist/**' '!docs/_generated/**'" }}- Step 5: Run + iterate
pnpm docs:spellFor each unknown word: add legitimate term to .cspell/ideony-dict.txt, fix genuine typos in MD.
- Step 6: Commit
git add cspell.json .cspell package.json docs pnpm-lock.yamlgit commit -m "feat(docs): cspell gate + Ideony dictionary"Task 4.4: lychee link check
Section titled “Task 4.4: lychee link check”Files:
-
Create:
lychee.toml -
Modify: root
package.json -
Step 1: Install lychee
brew install lychee || cargo install lychee- Step 2: Config
lychee.toml:
# Skip network calls by default in pre-commit; CI opts ininclude_verbatim = trueexclude_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 = 8cache = truemax_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
pnpm docs:linksFor each dead internal link: fix path or remove.
- Step 5: Commit
git add lychee.toml package.json docsgit commit -m "feat(docs): lychee link-check gate (offline pre-commit, online CI)"Task 4.5: Mermaid syntax check
Section titled “Task 4.5: Mermaid syntax check”Files:
-
Create:
scripts/docs-lint-mermaid.mjs -
Modify: root
package.json -
Step 1: Install mermaid-cli
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
pnpm add -w -D globby@<latest-stable>- Step 4: Add script
{ "scripts": { "docs:lint:mermaid": "node scripts/docs-lint-mermaid.mjs" }}- Step 5: Run
pnpm docs:lint:mermaidExpected (before Phase 6 diagram migration): likely zero mermaid blocks — pass trivially. After Phase 6: catches broken diagrams.
- Step 6: Commit
git add scripts/docs-lint-mermaid.mjs package.json pnpm-lock.yamlgit 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
pnpm docs:examplesExpected: “no blocks found” (Phase 4 baseline — no ts example tagged blocks yet).
- Step 5: Commit
git add scripts/docs-extract-examples.mjs tsconfig.docs-examples.json package.jsongit 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:
# Docs quality gates — fail fast on staged MD filesSTAGED_MD=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)if [ -n "$STAGED_MD" ]; then pnpm docs:validate-frontmatter pnpm docs:lintfi(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
git add package.json .husky/pre-commitgit commit -m "feat(docs): docs:quality aggregate + pre-commit integration"Task 4.8: CI workflow docs-quality.yml
Section titled “Task 4.8: CI workflow docs-quality.yml”Files:
-
Create:
.github/workflows/docs-quality.yml -
Step 1: Write workflow
name: docs-qualityon: 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
git add .github/workflows/docs-quality.ymlgit 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.
Task 4.9: CI workflow docs-deploy.yml
Section titled “Task 4.9: CI workflow docs-deploy.yml”Files:
-
Create:
.github/workflows/docs-deploy.yml -
Step 1: Write
name: docs-deployon: 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
git add .github/workflows/docs-deploy.ymlgit commit -m "feat(ci): docs-deploy workflow (Cloudflare Pages + smoke)"Task 4.10: Phase 4 wrap
Section titled “Task 4.10: Phase 4 wrap”- 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
git add CHANGELOG.md docs/status.mdgit 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
git add docs/research/2026-04-2x-docs-gate-audit.mdgit 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:
# Test: feat: commit type MUST stage CHANGELOG + status.md + roadmap.mdtest_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:
# 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 donefi-
Step 3: Test passes
-
Step 4: Commit
git add scripts/check-docs-updated.sh scripts/check-docs-updated.test.shgit commit -m "feat(docs-gate): feat: commits require CHANGELOG + status + roadmap"Task 5.3: Upgrade gate — other gap rows
Section titled “Task 5.3: Upgrade gate — other gap rows”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).
Task 5.4: Phase 5 wrap
Section titled “Task 5.4: Phase 5 wrap”- Step 1: CHANGELOG + status
CHANGELOG: note gate upgrades. status.md: Phase 5 → shipped.
- Step 2: Commit
git add CHANGELOG.md docs/status.mdgit 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
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
git add apps/docs/package.json apps/docs/astro.config.mjs pnpm-lock.yamlgit 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
mermaidblock
Example C4 Context:
```mermaidC4Context 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
pnpm -F docs build- Step 4: Commit
git add docs/architecture.mdgit 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
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
Task 6.4: Phase 6 wrap
Section titled “Task 6.4: Phase 6 wrap”- Step 1: CHANGELOG + status
- Architecture diagrams now Mermaid source (no PNG diagrams in docs/).docs/status.md: Phase 6 → shipped.
- Step 2: Commit
git add CHANGELOG.md docs/status.mdgit 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).
Task 7.1: Extract Drive content
Section titled “Task 7.1: Extract Drive content”Files:
-
Create:
docs/cofounder-overview.md -
Modify: individual
docs/specs/*.mdwhere 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 overviewquadrant: explanationaudience: cofounderlast_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
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.
Task 4.5.1: Change exit code in extractor
Section titled “Task 4.5.1: Change exit code in extractor”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
git add scripts/docs-extract-examples.mjs .github/workflows/docs-quality.ymlgit 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.mjsto import preset
Do not start until Jooice has a matching apps/docs/ to share with. Adds speculative complexity before.
Self-review (written 2026-04-21)
Section titled “Self-review (written 2026-04-21)”Spec coverage:
| Spec section | Tasks |
|---|---|
| §1 goals | covered by cumulative phases |
| §2 framework pick | §1 install + config (Tasks 1.1–1.3) |
| §3 repo layout | Task 1.1 (apps/docs/ scaffold), Task 1.3 (gitignore _generated/) |
| §4 auto-gen | Tasks 3.1–3.5 |
| §5 quality gates | Tasks 4.1–4.10 |
| §6 zero-drift audit | Tasks 5.1–5.4 |
| §7 agent layer | Tasks 2.1–2.4 |
| §8 diagrams | Tasks 6.1–6.4 |
| §9 cofounder access | Task 1.5 (Zero Trust), Task 7.1 (content migration) |
| §10 versioning | Out of scope pre-launch (spec §10 self-declares inactive) |
| §11 federation | Phase 8 outline |
| §12 CI + hosting | Tasks 4.8 (quality workflow), 4.9 (deploy workflow), 1.5 (CF Pages) |
| §13 rollout phases | Matched 1:1 to Phase 1–8 below |
| §14 risks | No task — risks documented in spec §14; mitigations built into tasks (worktree, gitignore, pre-commit) |
| §15 out of scope | Honored (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.
Execution notes
Section titled “Execution notes”- 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+ rootpackage.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.