l-update-generator
Detect and fix drift between the main zudo-doc project and the create-zudo-doc generator CLI. Use when adding/removing features, or to verify the generator stays in sync. Also triggered by "update gen...
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 @theme 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:
Copy the minimal base template (
templates/base/) — 5 files, no anchors:pages/,index. tsx pages/docs/[[...slug]].tsx,src/,styles/ global. css tsconfig.json,scripts/(the last is excluded from the unconditional mirror and copied only whensetup- doc- skill. sh skillSymlinkeris selected — seescaffold.ts'sEXCLUDE_FROM_MIRROR).Generate the ONE
zfb.config.tsprogrammatically (zfb-config-gen.ts) — diff-from-defaults: only fields whose value differs from the matchingZudoDocConfig@defaultare emitted (siteNameis always emitted). There is no moresettings-gen.tsand no moresrc/in a fresh scaffold.config/ settings. ts Compose selected features (
compose.ts+src/features/*.ts) — most feature modules are now pure settings-field emission (injections: [], nopostProcess) 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 explicitsrc/CLI/config module),config/ tag- vocabulary. ts tauri/tauriDev(Rust shells undersrc-tauri*/),skillSymlinker(scripts/, copied insetup- doc- skill. sh scaffold.tsdirectly, not atemplates/features/dir),claudeSkills(copies curatedzudo-doc-*Claude Code skills from the monorepo, also inscaffold.tsdirectly),claudeSkillsWriting(copies thezudo-doc-writingwriting/navigation-guide skill, also inscaffold.tsdirectly).postProcesspatches (small, targeted string patches to an already-generated file — never new files):docHistory(patches the doc-route stub(s) to statically import the realDocHistoryand thread it throughcreateChrome),designTokenPanel(inserts the one conditional@import "@takazudo/line intozdtp/ styles. css"; global.css— the one thing that can't ship unconditionally from@takazudo/, since it pulls in zdtp's own bytes),zudo- doc/ theme. css tagGovernance(writes the explicittag-vocabulary.tsmodule),tauri(patchesCargo.toml/tauri.conf.jsonnames + appends.gitignoreentries),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/:
Census (source of truth):
packages/—zudo- doc/ src/ config. ts ZudoDocConfig(the documented field surface, every field carries a@defaultJSDoc) andDEFAULT_SETTINGS(the actual default values).Generator:
packages/—create- zudo- doc/ src/ zfb- config- gen. ts DEFAULT_MIRROR(a hand-kept local copy ofDEFAULT_SETTINGS, only for fields the generator can ever set — it can'timportthe 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/ (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 dependenciesGenerator:
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. Remember: zod, preact, preact-render-to-string, diff, and @takazudo/zdtp are unconditional base deps now (each has a header comment explaining the "optional peer that's actually required at build time" trap — see generatePackageJson()).
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— spreadssettingsfromsrc/intoconfig/ settings. ts zudoDoc({...})Generator:
packages/— programmatic diff-from-defaults generation, samecreate- zudo- doc/ src/ zfb- config- gen. ts zudoDoc()entry point
Both sides go through the SAME zudoDoc() function (packages/), 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/ 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
ZudoDocConfigfield and no generator feature module backing it?Generator:
packages/— feature modules (mostly settings-field emission now; injections/postProcess only for the handful of genuine gaps listed in "Architecture Overview" above)create- zudo- doc/ src/ features/ *. ts Templates:
packages/— feature-specific files (onlycreate- zudo- doc/ templates/ features/ */ files/ i18n,tagGovernance,tauri,tauriDevstill have any)
For each feature-gated behavior in the main project, verify:
A feature module exists in
src/features/and is registered insrc/features/ index. ts If the feature ships genuine files, they exist in
templates/features/<name>/files/If the feature introduces a new plugin/collection,
packages/'szudo- doc/ src/ preset. ts zudoDocPreset()wires it from the matching settings field — NOT the generator (the generator only ever emits the field value)If the feature has a
postProcesshook, verify the string patch it applies still matches the current shape of the file it patches (e.g.docHistory's patch targets the exactcreateChrome(routeCtx);/import { createChrome } from "@takazudo/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)zudo- doc/ chrome";
1e. Base template drift
Compare the 5 base template files against their showcase / package counterparts (there is very little to compare now — most showcase behavior is package-owned, not template-mirrored):
templates/— 1-line re-exportbase/ pages/ index. tsx templates/— self-contained doc-route stub (compare against the showcase'sbase/ pages/ docs/ [[. . . slug]]. tsx pages/docs/[[...slug]].tsx, which the.template-drift-allowlistallows to diverge — the showcase addschromeBindingsModulewiring the minimal stub doesn't need)templates/— fixed ~20-linebase/ src/ styles/ global. css @importchain (compare against the showcase'ssrc/, also allowlisted — the showcase's is a superset)styles/ global. css templates/— 5-line extends form (compare against the showcase'sbase/ tsconfig. json tsconfig.json, allowlisted — the showcase's is a superset per thepaths/baseUrlGOTCHA documented inpackages/)zudo- doc/ CLAUDE. md templates/— should be byte-identical to the rootbase/ scripts/ setup- doc- skill. sh scripts/(the root copy IS the source template; not allowlisted, must match exactly)setup- doc- skill. sh
Automated first check: Run pnpm check:template-drift before doing manual analysis. This runs scripts/ and quickly identifies files that differ between the main project and the base template.
Allowlist note: The 6 pairs in .template-drift-allowlist (global.css, tsconfig.json, pages/, the two doc-route stubs, 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 any non-slot-section 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/) that fails if the template regresses to a pre-ramp-restructure token or drops the @import "@takazudo/ 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:
Config-field drift → Update
zfb-config-gen.ts'sDEFAULT_MIRROR+buildDesiredConfig()+FIELD_ORDER. If the field doesn't exist yet onZudoDocConfig, add it there first (with a@defaultJSDoc — enforced byconfig-jsdoc.test.ts) and toDEFAULT_SETTINGS.Dependency drift → Update
scaffold.tsgeneratePackageJson()to add/remove depsPreset drift → Update
packages/'szudo- doc/ src/ preset. ts zudoDocPreset()to wire the plugin/collection from the settings fieldFeature composition drift → Create/update feature module in
src/features/, register it insrc/, add template files only if there's a genuine gap (check whetherfeatures/ index. ts @takazudo/zudo-docalready ships the behavior first)Base template drift → Update the stale file in
templates/base/
After fixes:
Run
cd packages/create-zudo-doc && pnpm buildto verify TypeScript compilesRun
cd packages/create-zudo-doc && pnpm testto verify tests pass (including the settings-drift guard, which assertsZudoDocConfigis a subset ofFIELD_ORDER∪ a reasoned allowlist)Commit with message:
fix(create-zudo-doc): sync generator with main project
Key Files
| File | Role |
|---|---|
packages/ | Canonical field census — ZudoDocConfig (documented, @default-tagged) + DEFAULT_SETTINGS. Source of truth for every generated project's field surface. |
src/ | 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/ | The SINGLE config generator — emits defineConfig(zudoDoc({...})), diff-from-defaults against DEFAULT_MIRROR (a local, hand-kept copy of DEFAULT_SETTINGS) |
packages/ | Composition engine (injection system mostly unused — ANCHOR_FILES is []; kept as infra), feature resolution |
packages/ | Feature modules — settings-field emission via zfb-config-gen.ts, plus a handful of genuine file copies / postProcess patches |
packages/ | Registers every feature module |
packages/ | Orchestrates generation pipeline, generates package.json, .gitignore, .npmrc, seeds starter content |
packages/ | Generates the per-project CLAUDE.md |
packages/ | The locked ~12-file (barebone) minimal manifest's static half — 5 files, no @slot: anchors |
packages/ | Feature-specific files copied when a feature is enabled — only i18n, tagGovernance, tauri, tauriDev still have any |
packages/ | zudoDocPreset() — wires plugins/collections/markdown from settings fields; the generator never wires plugins directly |
packages/ | Manifest-shape assertions (exact 12-file barebone list, exact all-on list, settings-drift guard) |
packages/ | Field-mapping / diff-from-defaults unit tests |