# Update create-zudo-doc Generator

Detect and fix drift between the main zudo-doc project and the `create-zudo-doc` CLI generator.

## Architecture Overview (minimal-scaffold, epic zudolab/zudo-doc#2651)

The generator emits the **locked ~12-file minimal manifest** — one config
file (`zfb.config.ts`, `zudoDoc({ ...only fields the user chose })`) plus
markdown content plus a handful of unavoidable root files. Everything else
(layout, chrome, islands, default `:root` tokens, even the doc ROUTES
themselves via `packageOwnedRoutes`) ships from `@takazudo/zudo-doc` in
`node_modules`. There is no more copy-then-strip and no more `@slot:`
injection anchors:

1. Copy the minimal **base template** (`templates/base/`) — 10 files, no
   injection anchors: `pages/index.tsx`, `pages/docs/[[...slug]].tsx`,
   `src/styles/global.css`, `tsconfig.json`, `scripts/setup-doc-skill.sh`,
   `scripts/check-links.js`, and four `public/` favicon files
   (the last is excluded from the unconditional mirror and copied only when
   `skillSymlinker` is selected — see `scaffold.ts`'s `EXCLUDE_FROM_MIRROR`).
2. **Generate** the ONE `zfb.config.ts` programmatically (`zfb-config-gen.ts`)
   — diff-from-defaults: only fields whose value differs from the matching
   `ZudoDocConfig` `@default` are emitted (`siteName` is always emitted).
   There is no more `settings-gen.ts` and no more `src/config/settings.ts`
   in a fresh scaffold.
3. **Compose** selected features (`compose.ts` + `src/features/*.ts`) — most
   feature modules are now **pure settings-field emission** (`injections: []`,
   no `postProcess`) because the corresponding UI/behavior is entirely
   package-owned (package-first migration, epics #2321/#2344/#2356). Only a
   handful still do real work:
   - **Genuine file copies**: `i18n` (locale doc-route stub), `tagGovernance`
     (one explicit `src/config/tag-vocabulary.ts` CLI/config module), `tauri` /
     `tauriDev` (Rust shells under `src-tauri*/`), `skillSymlinker`
     (`scripts/setup-doc-skill.sh`, copied in `scaffold.ts` directly, not a
     `templates/features/` dir), `claudeSkills` (copies curated
     `zudo-doc-*` Claude Code skills from the monorepo, also in
     `scaffold.ts` directly), `claudeSkillsWriting` (copies the
     `zudo-doc-writing` writing/navigation-guide skill, also in
     `scaffold.ts` directly).
   - **`postProcess` patches** (small, targeted string patches to an
     already-generated file — never new files): `docHistory` (patches the
     doc-route stub(s) to statically import the real `DocHistory` and thread
     it through `createChrome`), `designTokenPanel` (inserts the one
     conditional `@import "@takazudo/zdtp/styles.css";` line into
     `global.css` — the one thing that can't ship unconditionally from
     `@takazudo/zudo-doc/theme.css`, since it pulls in zdtp's own bytes),
     `tagGovernance` (writes the explicit `tag-vocabulary.ts` module), `tauri`
     (patches `Cargo.toml`/`tauri.conf.json` names + appends `.gitignore`
     entries), `tauriDev` (similar name patching).

`tagGovernance` passes its module explicitly to the package-owned
`tags-audit`/`tags-suggest` bins with `--config`; there are no project-side CLI
shims and no duplicate settings module.

## When to Use

- After adding or removing a feature from zudo-doc
- When the drift-detection test fails
- Periodically to verify generator health
- User says "update generator", "sync generator", "check generator drift", "l-update-generator", or the old name "l-sync-create-zudo-doc"

**Quick pre-check**: Run `pnpm check:template-drift` first to get an automated summary of base template drift. Then proceed with the full workflow below for the config census, dependency, and feature composition drift.

## Step 1: Analyze Drift

Compare the main project's source files with what the generator produces.

### 1a. Settings/config-field drift

The field census now lives in ONE place — the package, not this repo's own
`src/config/settings.ts`:

- **Census (source of truth)**: `packages/zudo-doc/src/config.ts` —
  `ZudoDocConfig` (the documented field surface, every field carries a
  `@default` JSDoc) and browser-safe `settings-defaults.ts` (the actual `DEFAULT_SETTINGS`, re-exported by `config.ts`).
- **Generator**: `packages/create-zudo-doc/src/zfb-config-gen.ts` —
  `DEFAULT_MIRROR` (a hand-kept local copy of `DEFAULT_SETTINGS`, only for
  fields the generator can ever set — it can't `import` the package, see the
  file's header comment for why) + `buildDesiredConfig()` (user-choice →
  field mapping) + `FIELD_ORDER` (cosmetic emission order).

Compare field names between `ZudoDocConfig` and `zfb-config-gen.ts`'s
`DEFAULT_MIRROR`/`FIELD_ORDER`/`buildDesiredConfig()`. A field the generator
can plausibly set (has a CLI flag or prompt) but is missing from
`DEFAULT_MIRROR`/`FIELD_ORDER` is drift. A field `DEFAULT_MIRROR` carries
whose default value no longer matches `DEFAULT_SETTINGS` is also drift
(stale mirror).

This repo's OWN `src/config/settings.ts` (the showcase's real settings
object, spread into `zfb.config.ts`'s `zudoDoc({ ...settings, ... })`) is a
secondary, informal cross-check — useful for spotting a field the showcase
demonstrates but the generator never learned to emit — but it is NOT the
census. Don't chase drift against it that isn't also drift against
`ZudoDocConfig`.

### 1b. Dependency drift

Compare dependencies:

- **Main**: `package.json` — root dependencies
- **Generator**: `packages/create-zudo-doc/src/scaffold.ts` `generatePackageJson()` — generated deps

Check for packages used in base template files and feature files that are not included in the generated package.json. Read `generatePackageJson()` for the actual dependency branches: zfb family pins stay aligned; zod is required; zdtp is feature-selected, and 0.8.6 owns its Preact dependency. `diff` and `katex` remain optional runtime peers. No `preact-render-to-string` or React compatibility dependency is needed for owned JSX.

### 1c. zfb config / preset drift

Compare the main project's `zfb.config.ts` (`zudoDoc({ ...settings, chromeBindingsModule: "./src/chrome-bindings" })`) with what the generator produces:

- **Main**: `zfb.config.ts` — spreads `settings` from `src/config/settings.ts` into `zudoDoc({...})`
- **Generator**: `packages/create-zudo-doc/src/zfb-config-gen.ts` — programmatic diff-from-defaults generation, same `zudoDoc()` entry point

Both sides go through the SAME `zudoDoc()` function
(`packages/zudo-doc/src/config.ts`), so there is no separate "integration
wiring" to check field-by-field anymore — `zudoDocPreset()` (the internal
fragment builder `zudoDoc()` calls) is the one place collections/plugins/
markdown logic lives. Only check `packages/zudo-doc/src/preset.ts` when a
NEW plugin or collection needs wiring from a settings field (see 1d).

### 1d. Feature composition drift

Check that features in the main project have corresponding feature modules:

- **Main**: does the showcase demonstrate a behavior with no `ZudoDocConfig` field and no generator feature module backing it?
- **Generator**: `packages/create-zudo-doc/src/features/*.ts` — feature modules (mostly settings-field emission now; injections/postProcess only for the handful of genuine gaps listed in "Architecture Overview" above)
- **Templates**: `packages/create-zudo-doc/templates/features/*/files/` — feature-specific files (only `i18n`, `tagGovernance`, `tauri`, `tauriDev` still have any)

For each feature-gated behavior in the main project, verify:

1. A feature module exists in `src/features/` and is registered in `src/features/index.ts`
2. If the feature ships genuine files, they exist in `templates/features/<name>/files/`
3. If the feature introduces a new plugin/collection, `packages/zudo-doc/src/preset.ts`'s `zudoDocPreset()` wires it from the matching settings field — NOT the generator (the generator only ever emits the field value)
4. If the feature has a `postProcess` hook, verify the string patch it applies still matches the current shape of the file it patches (e.g. `docHistory`'s patch targets the exact `createChrome(routeCtx);` / `import { createChrome } from "@takazudo/zudo-doc/chrome";` lines in the doc-route stub — a stub rewrite elsewhere breaks this silently since the patch is a literal string match, not an AST edit)

### 1e. Base template drift

Compare the 6 source-code base template files against their showcase / package counterparts (the four favicon files are byte-identical assets; most showcase behavior is package-owned, not template-mirrored):

- `templates/base/pages/index.tsx` — 1-line re-export
- `templates/base/pages/docs/[[...slug]].tsx` — self-contained doc-route stub (compare against the showcase's `pages/docs/[[...slug]].tsx`, which the `.template-drift-allowlist` allows to diverge — the showcase adds `chromeBindingsModule` wiring the minimal stub doesn't need)
- `templates/base/src/styles/global.css` — fixed ~20-line `@import` chain (compare against the showcase's `src/styles/global.css`, also allowlisted — the showcase's is a superset)
- `templates/base/tsconfig.json` — 5-line extends form (compare against the showcase's `tsconfig.json`, allowlisted — the showcase's is a superset that keeps a TS5-only `baseUrl`, while the template must stay `baseUrl`-free with an explicit `"./src/*"` target — see "Owned JSX and project-local aliases" in `packages/zudo-doc/CLAUDE.md`)
- `templates/base/scripts/setup-doc-skill.sh` — should be byte-identical to the root `scripts/setup-doc-skill.sh` (the root copy IS the source template; not allowlisted, must match exactly)
- `templates/base/scripts/check-links.js` — source-first counterpart to the showcase checker; allowlisted because generated projects read literal settings from `zfb.config.ts` instead of the showcase's `src/config/settings.ts`, so validation-core changes require manual parity review

**Automated first check**: Run `pnpm check:template-drift` before doing manual analysis. This runs `scripts/check-template-drift.sh` and quickly identifies files that differ between the main project and the base template.

**Allowlist note**: The 7 pairs in `.template-drift-allowlist` (`global.css`, `tsconfig.json`, `pages/index.tsx`, the two doc-route stubs, `scripts/check-links.js`, and the `tauri` feature's orphaned find-in-page files) are skipped by the automated script's whole-file content check because they intentionally differ (the showcase carries wiring the minimal template doesn't need). These files **still require manual review** — check that validation-core or other relevant changes in the main project are reflected in the template counterpart. `global.css` also has a dedicated automated guard (`check_global_css_legacy_tokens` in `scripts/check-template-drift.sh`) that fails if the template regresses to a pre-ramp-restructure token or drops the `@import "@takazudo/zudo-doc/theme.css"` line — that one drift class stays automated even though the rest of the file needs manual review.

## Step 2: Report Findings

Present a clear drift report:

```
## Config-Field Drift
- Missing in generator: fieldA, fieldB (present in ZudoDocConfig, absent from DEFAULT_MIRROR/FIELD_ORDER)
- Stale default in DEFAULT_MIRROR: fieldC (generator default no longer matches DEFAULT_SETTINGS)

## Dependency Drift
- Missing from generated package.json: packageX (used unconditionally, or by featureY)
- Unnecessary in generated package.json: packageZ (feature disabled / no longer needed)

## Preset Drift
- Missing plugin wiring: pluginX in packages/zudo-doc/src/preset.ts for settings.fieldY

## Feature Composition Drift
- Missing feature module: feature "X" has a ZudoDocConfig field but no src/features/<name>.ts
- Missing template files: feature "X" module exists but templates/features/X/files/ is missing a file it should copy
- Stale postProcess patch: feature "X"'s string-replace target no longer exists in the file it patches

## Base Template Drift
- Stale file: templates/base/<path> differs from its showcase/package counterpart

## No Drift Detected
(if everything is in sync)
```

## Step 3: Apply Fixes

For each drift item found:

1. **Config-field drift** → Update `zfb-config-gen.ts`'s `DEFAULT_MIRROR` + `buildDesiredConfig()` + `FIELD_ORDER`. If the field doesn't exist yet on `ZudoDocConfig`, add it there first (with a `@default` JSDoc — enforced by `config-jsdoc.test.ts`) and to `settings-defaults.ts`'s `DEFAULT_SETTINGS`.
2. **Dependency drift** → Update `scaffold.ts` `generatePackageJson()` to add/remove deps
3. **Preset drift** → Update `packages/zudo-doc/src/preset.ts`'s `zudoDocPreset()` to wire the plugin/collection from the settings field
4. **Feature composition drift** → Create/update feature module in `src/features/`, register it in `src/features/index.ts`, add template files only if there's a genuine gap (check whether `@takazudo/zudo-doc` already ships the behavior first)
5. **Base template drift** → Update the stale file in `templates/base/`

After fixes:

1. Run `cd packages/create-zudo-doc && pnpm build` to verify TypeScript compiles
2. Run `cd packages/create-zudo-doc && pnpm test` to verify tests pass (including the settings-drift guard, which asserts `ZudoDocConfig` is a subset of `FIELD_ORDER` ∪ a reasoned allowlist)
3. Commit with message: `fix(create-zudo-doc): sync generator with main project`

## Key Files

| File | Role |
|------|------|
| `packages/zudo-doc/src/config.ts` | Canonical field census — `ZudoDocConfig` (documented, `@default`-tagged); browser-safe `settings-defaults.ts` owns `DEFAULT_SETTINGS`. Source of truth for every generated project's field surface. |
| `src/config/settings.ts` | This repo's own showcase settings (informal cross-check only — see Step 1a) |
| `zfb.config.ts` | Main project zfb config — `zudoDoc({ ...settings, chromeBindingsModule: "./src/chrome-bindings" })` |
| `packages/create-zudo-doc/src/zfb-config-gen.ts` | The SINGLE config generator — emits `defineConfig(zudoDoc({...}))`, diff-from-defaults against `DEFAULT_MIRROR` (a local, hand-kept copy of `DEFAULT_SETTINGS`) |
| `packages/create-zudo-doc/src/compose.ts` | Composition engine (injection system mostly unused — `ANCHOR_FILES` is `[]`; kept as infra), feature resolution |
| `packages/create-zudo-doc/src/features/*.ts` | Feature modules — settings-field emission via `zfb-config-gen.ts`, plus a handful of genuine file copies / `postProcess` patches |
| `packages/create-zudo-doc/src/features/index.ts` | Registers every feature module |
| `packages/create-zudo-doc/src/scaffold.ts` | Orchestrates generation pipeline, generates `package.json`, `.gitignore`, `.npmrc`, seeds starter content |
| `packages/create-zudo-doc/src/claude-md-gen.ts` | Generates the per-project `CLAUDE.md` |
| `packages/create-zudo-doc/templates/base/` | The locked ~18-file (barebone) minimal manifest's static half — 10 files, no `@slot:` anchors |
| `packages/create-zudo-doc/templates/features/` | Feature-specific files copied when a feature is enabled — only `i18n`, `tagGovernance`, `tauri`, `tauriDev` still have any |
| `packages/zudo-doc/src/preset.ts` | `zudoDocPreset()` — wires plugins/collections/markdown from settings fields; the generator never wires plugins directly |
| `packages/create-zudo-doc/src/__tests__/scaffold.test.ts` | Manifest-shape assertions (exact 12-file barebone list, exact all-on list, settings-drift guard) |
| `packages/create-zudo-doc/src/__tests__/zfb-config-gen.test.ts` | Field-mapping / diff-from-defaults unit tests |