# Scope: Fix prebuild source mutation

**Created by:** Ana
**Date:** 2026-05-15

## Intent

The website prebuild script (`website/scripts/extract-docs-data.ts`) has a hidden side effect: `updateDynamicMdxValues()` rewrites tracked MDX source files, replacing `{/* ana:dynamic ... */}` markers with computed values. This causes silent dirty state on every build, guaranteed rebase conflicts on docs-touching branches, and maintains a redundant data path — every value hardcoded into MDX source already exists as a runtime function in `website/lib/docs-data/`. Separately, three generated files in `public/` (llms.txt, llms-full.txt, search-index.json) are tracked in git despite being regenerated by every prebuild run, compounding the same rebase-conflict problem.

The user wants the prebuild to stop mutating tracked files entirely. Dynamic values should flow from their single source of truth (`lib/docs-data`) through server components at SSG time, and all generated output files should be gitignored.

## Complexity Assessment

- **Kind:** fix
- **Size:** medium — 13 files across 1 package, no new systems, structural analog exists
- **Files affected:**
  - `website/scripts/extract-docs-data.ts` — delete `updateDynamicMdxValues`, add component-tag resolution to local `stripJsx`
  - `website/lib/docs-data/stripJsx.ts` — add component-tag resolution for copy-as-markdown
  - `website/app/docs/[...slug]/page.tsx` — register new component in `mdxComponents`
  - `website/components/docs/content/` — new server component file
  - `website/.gitignore` — add 3 public file patterns
  - 7 MDX files (pipeline.mdx, start.mdx, troubleshooting.mdx, verifying-changes.mdx, using-ana-learn.mdx, reading-a-proof.mdx, skills.mdx) — replace markers with component usage
- **Blast radius:** Rendered docs pages, llms-full.txt content, copy-as-markdown output, SearchOverlay (fetches search-index.json — no change since file is still generated to public/ by prebuild, just not tracked). No CLI package impact. No CI config changes.
- **Estimated effort:** ~2 hours build + verify
- **Multi-phase:** no

## Approach

Delete the source-mutation function and replace it with a server component that reads from the existing `lib/docs-data` functions at SSG time. The component renders inline (`<span>`) so it works inside `<p>` tags and headings. MDX files become pure authored content — no generated numbers, no comment markers. Two MDX callsites where the dynamic value is inside a string prop (NextCards `description` fields) need structural refactoring since components can't render inside string literals.

For the two non-rendered consumers of raw MDX (llms-full.txt generation and copy-as-markdown), add a shared resolution step to both `stripJsx` implementations that replaces component tags with computed text values before stripping other JSX. The prebuild script already has all computed data in scope; the lib version can call `lib/docs-data` functions at build time (SSG).

Gitignore the three tracked generated public files and remove them from git tracking. They are regenerated by the prebuild lifecycle hook before every build (local, CI, Vercel) — the same pattern already used for `data/docs/`.

## Acceptance Criteria

- AC1: Running `pnpm build` in `website/` produces zero unstaged changes to tracked files (MDX files and public/ are clean after build)
- AC2: All 9 dynamic values render correctly on their respective docs pages with current data from `lib/docs-data` functions
- AC3: `public/llms-full.txt` contains resolved numeric values (not component tags, not empty strings) for all 9 dynamic positions
- AC4: Copy-as-markdown output (RightRail `pageContent`) contains resolved numeric values for pages with dynamic content
- AC5: `updateDynamicMdxValues` function no longer exists in `extract-docs-data.ts`
- AC6: `public/llms.txt`, `public/llms-full.txt`, and `public/search-index.json` are in `.gitignore` and not tracked by git
- AC7: `pnpm build` succeeds end-to-end (prebuild + next build) with no errors
- AC8: SearchOverlay client-side fetch of `/search-index.json` still works (file generated at build time, served from public/)

## Edge Cases & Risks

- **medianTimings renders inside a `<p>` with inline styles.** The component must render as a `<span>` (inline), not a `<div>` (block), or it breaks HTML nesting rules. The rendered output is a complete sentence with 5 computed values.
- **rejectionProofCount has two prose variants.** Three pages use "Anatomia's own" and one uses "our" as the possessive. The surrounding prose is authored content — it stays in MDX. The component only provides the numbers.
- **Two markers are inside string-literal props.** `using-ana-learn.mdx:116` and `troubleshooting.mdx:133` embed dynamic values in NextCards `description` strings. A React component cannot render inside a JS string. These two callsites need structural refactoring — either NextCards accepts ReactNode for `description`, or these specific card items compute their own description.
- **Removing files from git tracking.** `git rm --cached` on the 3 public files means other branches with these files tracked will see a merge artifact on rebase. This is a one-time cost and trivially resolved (files are regenerated). NOT fixing it means every branch pays the rebase-conflict cost forever.
- **Dev mode without prebuild.** `pnpm dev` doesn't trigger prebuild (no `predev` script). Developers must run the prebuild once or `pnpm build` to populate `data/docs/`. This is an existing constraint, not introduced by this fix, but the new component will fail with a clear error (missing data file) rather than silently showing stale numbers.
- **stripJsx has two copies.** One in `extract-docs-data.ts` (prebuild, has data in scope) and one in `lib/docs-data/stripJsx.ts` (runtime/SSG, can import from lib). Both need the resolution step. The resolution map (tag name to computed text) should be shared or derived from the same data functions to avoid drift.

## Rejected Approaches

**Remark plugin to resolve markers at MDX compile time.** Would avoid changing MDX source but remains regex-based, is less discoverable than components, and doesn't solve the two non-rendered consumers (llms-full.txt, copy-as-markdown). Those would still need separate resolution. Components solve all three consumers with one pattern.

**Keep mutation but commit updated numbers as a release step.** Trades build-time dirty state for release-process complexity. Doesn't eliminate rebase conflicts — just moves them to a controlled moment. The data path remains duplicated (numbers in source AND in lib functions). Treats the symptom.

**Replace markers with MDX expression syntax.** MDX supports `{expression}` but fumadocs-mdx compilation doesn't expose server-side data in the MDX expression scope. Would require a custom MDX provider or loader plugin — heavier than a component.

## Open Questions

- Exact component API: single `tag` prop with a lookup map, or `source` + `format` props for more explicit control? Plan should evaluate what's simplest for 6 tag types.
- How to refactor the 2 NextCards string-prop callsites: should NextCards accept ReactNode for `description`, or should those specific items be extracted into a wrapper component that computes its own text? Plan should check how NextCards renders `description` before deciding.
- Should the tag-to-text resolution map live alongside the component, in a shared module, or inline in each stripJsx implementation? Tradeoff: shared module is DRY but adds a file; inline is self-contained but drifts.

## Exploration Findings

### Patterns Discovered

- `website/app/docs/page.tsx` — the docs overview page already calls `getProofStats()`, `getSkillCount()`, etc. and renders values directly in JSX. This is the correct pattern; the MDX pages should work the same way.
- `website/app/docs/[...slug]/page.tsx:21-32` — `mdxComponents` object maps component names to React components for MDX rendering. New components register here.
- `website/lib/docs-data/proofs.ts` — `getProofStats()` returns `{ entries, assertions, findings, rejections }`, `getMedianTimings()` returns `{ think, plan, build, verify }`. Exact duplicates of the computation in `updateDynamicMdxValues`.
- `website/lib/docs-data/gotchas.ts:21` — `getGotchaCount()` exists. `website/lib/docs-data/skills.ts:25` — `getSkillCount()` exists. All 6 tag types have existing data functions.

### Constraints Discovered

- [TYPE-VERIFIED] Inline rendering required (`reading-a-proof.mdx:66`) — medianTimings marker is inside `<p style={{...}}>`. Component must produce inline HTML.
- [TYPE-VERIFIED] String-prop barrier (`using-ana-learn.mdx:116`, `troubleshooting.mdx:133`) — `proofFindings` and `proofSummary` are inside `description: "..."` string props on NextCards. Components cannot render inside string literals.
- [TYPE-VERIFIED] Dual stripJsx (`scripts/extract-docs-data.ts:814-843`, `lib/docs-data/stripJsx.ts:8-59`) — two independent implementations, both strip self-closing JSX components on line 821/18 respectively. Both need resolution logic.
- [OBSERVED] llms-full.txt reads post-mutation MDX — `generateLlmsTxt` (line 925-943) reads raw MDX files AFTER `updateDynamicMdxValues` runs (line 1171 vs 1181). Execution order dependency.
- [OBSERVED] Four rejectionProofCount instances, not three — pipeline.mdx:24, start.mdx:75, troubleshooting.mdx:43, verifying-changes.mdx:67. Two prose variants: "Anatomia's own" (3x) and "our" (1x).

### Test Infrastructure

- No existing tests for `updateDynamicMdxValues` or `stripJsx`. The `pnpm build` end-to-end (prebuild + next build) is the functional test. The smoke script (`scripts/smoke-test.sh`) may cover rendered output.

## For AnaPlan

### Structural Analog

`website/app/docs/page.tsx` — the docs overview page. It imports from `lib/docs-data`, calls data functions, and renders computed values in JSX. The new component does exactly this but from within MDX via the mdxComponents registry. Same data source, same rendering model, smaller surface.

### Relevant Code Paths

- `website/scripts/extract-docs-data.ts:956-1053` — the function to delete (`updateDynamicMdxValues`)
- `website/scripts/extract-docs-data.ts:814-843` — prebuild `stripJsx` (needs resolution step)
- `website/scripts/extract-docs-data.ts:1170-1185` — main() call site and generated file writes
- `website/lib/docs-data/stripJsx.ts` — runtime `stripJsx` (needs resolution step)
- `website/lib/docs-data/proofs.ts` — `getProofStats()`, `getMedianTimings()` (data source)
- `website/lib/docs-data/skills.ts:25` — `getSkillCount()` (data source)
- `website/lib/docs-data/gotchas.ts:21` — `getGotchaCount()` (data source)
- `website/app/docs/[...slug]/page.tsx:21-32` — mdxComponents registration
- `website/app/docs/[...slug]/page.tsx:49-56` — copy-as-markdown raw MDX reader

### Patterns to Follow

- `website/app/docs/page.tsx` — how the overview page calls data functions and renders values
- `website/components/docs/content/StatsStrip.tsx` — existing server component registered in mdxComponents, good reference for component structure and registration
- `website/.gitignore:42` — `data/docs/` is already gitignored with the same rationale (prebuild output)

### Known Gotchas

- The prebuild `stripJsx` function and the `lib/docs-data/stripJsx.ts` function are near-identical but independent copies. Both must be updated, and the resolution logic should be consistent. Consider whether they can share the resolution map without coupling the prebuild script to the lib module.
- `NextCards` is in the `stripFull` list in both `stripJsx` implementations — its content is fully stripped. If the refactored dynamic-description card uses a different wrapper component, that component needs to be handled by stripJsx too (either preserved or resolved).
- The `updateDynamicMdxValues` call (line 1171) runs BEFORE `generateLlmsTxt` (line 1181). After deletion, the llms.txt generator must resolve component tags in its own text-processing pass, not rely on pre-mutated source.

### Things to Investigate

- How does NextCards render `description` — as `{description}` (ReactNode-compatible) or via string methods? This determines whether accepting ReactNode is a one-line change or requires component refactoring.
- Whether a shared `resolveDynamicTags(text, data)` utility makes sense, or whether each stripJsx should inline its own resolution. Depends on whether the prebuild script can cleanly import from lib.
