design-RFC-011 — kinds-framework refactor for design-gen

ratified

design-gen wires ~29 page renderers by hand in cmd/design-gen/main.go, each as a repetitive stats.Phase(...) block. This RFC ratifies a self-registration framework (Option A) for those page renderers so a new renderer is added by dropping a file that calls kinds.Register in init(), not by editing main.go. KEY CORRECTION to ticket #109: new SPEC families already render with zero code via the generic spec-doc fallback — the real boilerplate is main.go's manual page-renderer wiring, not per-spec kinds. Recommends Option A (registration) over Option B (convention), because Option B's zero-code path already exists.

Ticket #109 names the RFC design-RFC-009-kinds-framework; that number was already taken (incremental-build) and 010 is the KDS MCP server, so this RFC is 011. Same content.

Abstract

Ticket #109 frames design-gen as having "30+ hardcoded kinds" whose boilerplate will become the bottleneck at 50 kinds. Walking the code first (the discipline that corrected #117) shows the framing conflates two very different things, and that the worst friction is narrower than stated. This RFC corrects the model and ratifies a self-registration framework for standalone page renderers (the ticket's Option A), while documenting that the ticket's Option B (convention-over-config for spec kinds) is already realized by the generic spec-doc fallback.

What "kinds" actually are (the correction)

`internal/kinds }


Each `kinds/<x>.go` self-registers in `init()`:

func init() { Register(PageRenderer{Name: "voice", Order: 40, Run: renderVoicePhase}) }


`main.go` collapses the ~29 hand-wired blocks into:

for _, p := range kinds.Registered() { if !enabled(p.Name) { continue } /honors --only if err := stats.Phase(p.Name, func() error { return p.Run(ctx, r, specs) }); err != nil { return err } }


Renderers with extra inputs (e.g. `RenderIcons` needs `repoRoot`,
`RenderMigrationGuide` needs from/to versions) capture them via closure
at registration or read them from the `Renderer` — the signature stays
uniform. Genuinely one-off entry points (migration guide, PDF) that
aren't part of the standard `make gen` sweep stay as explicit calls.

### Why NOT Option B as the primary model

Option B (frontmatter `kind:` → `kinds/<kind>.templ`, no Go) targets
spec-render kinds. But the zero-Go path for spec families **already
exists** as the `spec-doc` fallback — `RenderSpecDoc` renders any
unrecognized spec from frontmatter + markdown. Option B would only
re-skin that existing mechanism. The spec kinds that have their own
renderer (`ui-style`, `theme`, `icon`, …) do so because they have
**genuinely custom logic** (preset swatches, ΔE contrast, icon
generation) that a pure template can't express. So Option B buys little
the fallback doesn't already provide. Keep the spec-doc fallback as the
convention path; don't build a parallel template-only dispatcher.

## Scope guard (per reuse-first.kmd)

The registry is a small internal mechanism, not a new SDK — it has one
consumer (design-gen) and stays in `internal/kinds`. No
`reuse-first.kmd` SDK-extraction trigger (that needs ≥3 consumers).

## Migration (incremental, no behavior change)

1. Land `registry.go` + the main.go loop; migrate ONE small renderer as
   proof (`voice` or `motion` — single page, no extra inputs). main.go
   keeps calling the not-yet-migrated renderers explicitly during the
   transition (registry + explicit calls coexist).
2. Migrate the remaining page renderers ticket-by-ticket (#109.N), each
   asserting byte-identical `dist/` output via the regression suite.
3. Leave custom one-offs (migration guide, PDF, per-spec ui-style/theme
   loop) as explicit calls — they aren't part of the uniform sweep.

No rendered-output change at any step; the regression suite
(`tests/regression`) is the gate.

## Sub-tickets (opened with this RFC)

| ID | Title | Effort |
|---|---|---|
| #109.1 | `registry.go` + main.go sweep loop; migrate `voice` as proof (output-identical) | 1 day |
| #109.2 | Migrate remaining standalone page renderers to registry (batched) | 2-3 days |
| #109.3 | `tools/design-gen/docs/adding-a-kind.kmd` — author guide (register vs spec-doc fallback decision tree) | 0.5 day |

## Out of scope

- Migrate all renderers in one commit (incremental per #109.2).
- Replace the per-spec `ui-style`/`theme` custom renderers or the
  spec-doc fallback (custom logic stays).
- A template-only Option B dispatcher (the spec-doc fallback already
  covers the zero-code case).

## Acceptance

- [ ] This RFC ratified (model correction + Option A decision).
- [ ] `registry.go` + main.go loop landed; `voice` migrated as proof.
- [ ] `dist/` output byte-identical (regression suite green).
- [ ] `docs/adding-a-kind.kmd` documents: "new spec family → nothing
      (spec-doc renders it); new bespoke page → drop a file that calls
      Register in init()".