From 3fef7c22e8210b1e9b0589e8eb4665f00b814dc3 Mon Sep 17 00:00:00 2001 From: martyy-code Date: Thu, 30 Jul 2026 14:52:24 +0200 Subject: [PATCH 1/3] docs(studio): decompose the draft/preview/admin report into per-subsystem docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-07-30 architecture report was one 653-line file covering seven subsystems, so answering any single question meant loading all of it. It is now docs/reports/studio/ — 21 documents across admin/, agent/, preview/, publish/ and repo/, following the docs/learnings/ convention (README index table, numbered reading order, per-document Sources with verification dates). The original file becomes a stub with a section-to-file map, so old links land somewhere useful. This is not a copy. Every factual claim was re-measured against the repo on 2026-07-30 and several did not survive: - The dual-tree arbitration was too broad. "The showcase file is a re-export with no design content to lose" holds for 4 items of 9; five carry real implementation, and for input, textarea, colored-badge and icon-button there is no @ui/ primitive to re-export at all. Arbitration is now per item, and for icon-button and colored-badge the workspace copy wins — the consumer copies are the degraded ones (icon-button inlines a strict subset of the button base string). - The drift apparatus was not guarding "one import specifier". Its per-item assertions cover 3 of 9 items; three items get zero assertions and the run still reports green, which is why a real divergence in ds-icon-button has been sitting in a passing build. - "All nine items begin with use client" is true of the showcase tree and false of the shipped one, where it is 5 of 9. - contract-test.mjs already derives its item list dynamically (9/9). Only its npm install list is hardcoded, and that list is currently complete — a latent coupling, not a stale list. - The v16.2.6 release page does not carry the CVE-2026-44576 fix; that ships in v16.2.5 and v16.2.6 is a Turbopack follow-up. The recommendation to move to 16.2.12 is unchanged. - The button cva has 8 sizes, not nine. Two additions the decomposition surfaced: the validator set grows from six to eight (manifest-versus-imports agreement, and category in the closed set of 13 — both currently maintained by hand and losing their maintainer under decision 2), and the publish step has to write apps/web/lib/registry/index.tsx, a hand-maintained aggregator with 18 hardcoded imports that the original file list omitted. Also amends the shadcn-registry-adoption addendum, which repeated the first two corrections above, and fixes .gitignore: inline comments are not supported, so the .contract-test/ rule never matched. temp/saas-template is now ignored — it is a vendored checkout with its own .git. No code touched. Co-Authored-By: Claude --- .claude/agent-memory/main/MEMORY.md | 1 + .../main/project_design_learnings.md | 2 +- .../main/project_studio_decisions.md | 47 ++++ .gitignore | 5 +- .../2026-07-29-shadcn-registry-adoption.md | 124 ++++++++++ ...-07-30-draft-preview-admin-architecture.md | 64 ++++++ docs/reports/studio/01-data-model.md | 148 ++++++++++++ docs/reports/studio/02-single-tree.md | 216 ++++++++++++++++++ docs/reports/studio/99-frictions-and-costs.md | 141 ++++++++++++ docs/reports/studio/README.md | 156 +++++++++++++ docs/reports/studio/admin/01-auth.md | 119 ++++++++++ docs/reports/studio/admin/README.md | 155 +++++++++++++ docs/reports/studio/agent/01-validators.md | 206 +++++++++++++++++ docs/reports/studio/agent/02-cli.md | 143 ++++++++++++ docs/reports/studio/agent/03-mcp.md | 119 ++++++++++ docs/reports/studio/agent/README.md | 172 ++++++++++++++ .../preview/01-transpile-and-imports.md | 138 +++++++++++ docs/reports/studio/preview/02-css-compile.md | 183 +++++++++++++++ docs/reports/studio/preview/03-security.md | 142 ++++++++++++ docs/reports/studio/preview/README.md | 137 +++++++++++ .../studio/publish/01-shadcn-registry.md | 185 +++++++++++++++ docs/reports/studio/publish/README.md | 192 ++++++++++++++++ docs/reports/studio/repo/01-decoupling.md | 193 ++++++++++++++++ .../studio/repo/02-pnpm-and-versions.md | 188 +++++++++++++++ docs/reports/studio/repo/03-nextjs-16.md | 148 ++++++++++++ docs/reports/studio/repo/04-template-reuse.md | 147 ++++++++++++ docs/reports/studio/repo/README.md | 83 +++++++ 27 files changed, 3552 insertions(+), 2 deletions(-) create mode 100644 .claude/agent-memory/main/project_studio_decisions.md create mode 100644 docs/reports/2026-07-30-draft-preview-admin-architecture.md create mode 100644 docs/reports/studio/01-data-model.md create mode 100644 docs/reports/studio/02-single-tree.md create mode 100644 docs/reports/studio/99-frictions-and-costs.md create mode 100644 docs/reports/studio/README.md create mode 100644 docs/reports/studio/admin/01-auth.md create mode 100644 docs/reports/studio/admin/README.md create mode 100644 docs/reports/studio/agent/01-validators.md create mode 100644 docs/reports/studio/agent/02-cli.md create mode 100644 docs/reports/studio/agent/03-mcp.md create mode 100644 docs/reports/studio/agent/README.md create mode 100644 docs/reports/studio/preview/01-transpile-and-imports.md create mode 100644 docs/reports/studio/preview/02-css-compile.md create mode 100644 docs/reports/studio/preview/03-security.md create mode 100644 docs/reports/studio/preview/README.md create mode 100644 docs/reports/studio/publish/01-shadcn-registry.md create mode 100644 docs/reports/studio/publish/README.md create mode 100644 docs/reports/studio/repo/01-decoupling.md create mode 100644 docs/reports/studio/repo/02-pnpm-and-versions.md create mode 100644 docs/reports/studio/repo/03-nextjs-16.md create mode 100644 docs/reports/studio/repo/04-template-reuse.md create mode 100644 docs/reports/studio/repo/README.md diff --git a/.claude/agent-memory/main/MEMORY.md b/.claude/agent-memory/main/MEMORY.md index 8ea551e..68361a2 100644 --- a/.claude/agent-memory/main/MEMORY.md +++ b/.claude/agent-memory/main/MEMORY.md @@ -5,3 +5,4 @@ - [Registry deps coupling](feedback_registry_deps_coupling.md) — new registry item with new peer dep requires updating contract-test.mjs install list in lockstep - [Design learnings repo](project_design_learnings.md) — knowledge base + working monorepo for deessejs/ui registry at ui.deessejs.com (hosted on Vercel) - [Phase 4 validated](project_phase4_validated.md) — external install end-to-end confirmed 2026-07-29; Phase 6 (official shadcn index submission) gate is now lifted +- [Studio decisions](project_studio_decisions.md) — 9 locked decisions (2026-07-30): agents author into a DB, PR publishing, dual tree collapsed diff --git a/.claude/agent-memory/main/project_design_learnings.md b/.claude/agent-memory/main/project_design_learnings.md index f6f2828..0adb31f 100644 --- a/.claude/agent-memory/main/project_design_learnings.md +++ b/.claude/agent-memory/main/project_design_learnings.md @@ -23,7 +23,7 @@ This repo (`design/`) is both a **knowledge base** and the **working monorepo** - `learnings/` — research notes (Tailwind, shadcn, layout, page-content, marketing-ui, agent-system). Source URL + verification date convention. - `apps/web/` — Next.js 16 showcase site. Header, footer, nav, cards, code-block (Shiki), pager (previous/next), all on shadcn/Base UI + Tailwind v4. - `packages/ui/` — shadcn primitives (Base UI, not Radix), tokens, globals.css. Don't touch — this is the foundation. -- `packages/registry/` — deessejs registry components. Currently has Button (re-export) + IconButton + ColoredBadge (real impl). Each component has `index.tsx` (component + Demo export) and `meta.ts` (ComponentMeta). +- `packages/registry/` — deessejs registry components. 8 components (button, colored-badge, icon-button, breadcrumb, empty, tabs, input, textarea) + 1 block (empty-state), as of 2026-07-30. Each has `index.tsx` (component + Demo export) and `meta.ts` (ComponentMeta). - `apps/web/lib/registry/` — types, sources, aggregator. The seam for future DB-backed registry. - `apps/web/scripts/build-sources.mjs` — build-time codegen that reads `packages/registry/src/**/*.tsx` and emits `apps/web/lib/registry/sources.generated.ts`. diff --git a/.claude/agent-memory/main/project_studio_decisions.md b/.claude/agent-memory/main/project_studio_decisions.md new file mode 100644 index 0000000..62b4655 --- /dev/null +++ b/.claude/agent-memory/main/project_studio_decisions.md @@ -0,0 +1,47 @@ +--- +name: project-studio-decisions +description: Nine locked decisions (2026-07-30) for the agent-authored draft/preview studio — DB as authoring surface, PR publishing, collapsed dual tree +metadata: + type: project +--- + +Nine decisions locked on **2026-07-30** for the draft/preview/admin layer. Full reasoning, per-subsystem, +lives in `docs/reports/studio/` — start at its `README.md`. The original single-file report at +`docs/reports/2026-07-30-draft-preview-admin-architecture.md` is now a stub pointing there. + +**Why this exists:** components in this repo are authored by **AI agents, not by hand**. The +maintainer is the reviewer. The friction being removed is having to open an editor, launch an +agent, and run `next dev` (~1 GB RAM) just to see whether a component looks right. + +## The decisions + +1. The agent runs **locally**. No server-side agent orchestration — Studio is an API + a preview surface. +2. The **database is the authoring surface**. The agent writes no files. +3. Publishing **opens a PR**. The public registry stays static JSON — no Postgres on the consumer install path. +4. The agent contract is **both a CLI and an MCP server** over one core library. +5. The **dual tree collapses into one** (see [[project-design-learnings]] — this reverses a locked decision). + Arbitration is **per item**, not blanket: `icon-button` and `colored-badge` are the two where the + consumer copy is the degraded one, so the workspace copy wins there. +6. **One account**, `emailAndPassword.disableSignUp: true`. No `admin()` plugin, no roles, no email. +7. Migrate the repo to **pnpm 11 + catalogs** (the saas-template's model). +8. `registryDependencies` **always resolve to the published version** — a draft block whose dependency is still a draft cannot preview. +9. CSS is **compiled server-side** per draft version, not by `@tailwindcss/browser`. + +## Consequences that are easy to forget + +- **Live rendering is the product, not a later phase.** If the agent writes and the human only + reviews, the human's entire job is visual. +- **Write-path validators are the only gate.** With no PR between generated code and stored state, + token discipline / import allow-list / self-containment / required `Demo` must be enforced in the + oRPC mutation or they cease to exist. +- **Three origins are mandatory**, not stylistic: `ui.` (public), `studio.` (session), `preview.` + (cookieless, runs untrusted code). Same-origin preview = session theft. +- **Tailwind's `compile()` is undocumented internal API.** Maintainers said so on record; the return + field was renamed in a *minor* (4.1.0). Pin the exact version, contract-test the output, isolate + behind one module. +- **Decision 8 imposes a sequence**: a new primitive must be published (PR + CI + redeploy, ~4 min) + before a block using it can preview. Version pinning is the escape hatch if that becomes common. + +**How to apply:** treat these as settled — do not re-derive or re-litigate them. Phase 0 (decoupling +the hardcoded item lists, deleting the drift apparatus, pnpm migration, Next → 16.2.12) is blocking +for everything else. Related: [[project-design-learnings]], [[feedback-registry-deps-coupling]]. diff --git a/.gitignore b/.gitignore index 644713f..d8728e7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,11 @@ node_modules # testing coverage -.contract-test/ # ephemeral shim project created by apps/web/scripts/contract-test.mjs +# ephemeral shim project created by apps/web/scripts/contract-test.mjs +.contract-test/ temp/sandbox-validate +# vendored reference checkout, has its own .git — never commit +temp/saas-template # next.js .next/ diff --git a/docs/plans/2026-07-29-shadcn-registry-adoption.md b/docs/plans/2026-07-29-shadcn-registry-adoption.md index ff45fed..f610a6a 100644 --- a/docs/plans/2026-07-29-shadcn-registry-adoption.md +++ b/docs/plans/2026-07-29-shadcn-registry-adoption.md @@ -353,3 +353,127 @@ The deferred items from the **Phase 7** section (multi-style, multi-base, blocks - **Buggy monorepo surface in the shadcn CLI.** Multiple open 2026 bugs (#9239, #11002, related). Mitigation: authored consumer sources ship as-is — no runtime transform of workspace imports. The decoupling is the defense. - **Drift between `packages/registry/src/components//index.tsx` (showcase) and `registry/base-nova/ds-/ds-.tsx` (registry).** Mitigation: drift-check script added if drift shows up in Phase 5. - **Schema evolution.** The `registry.json` schema may grow new required fields. The validator in Phase 5 catches drift per CI run. + +--- + +# Addendum — 2026-07-30: the dual-tree decision is reopened and reversed + +**Status of this addendum:** supersedes the "Existing showcase tree" entry in the decisions log +above. Everything else in this plan stands. + +## What was locked + +From the decisions log (2026-07-29): + +> **Existing showcase tree:** `packages/registry/src/components//` and its `meta.ts` files stay +> untouched. […] `registry.json` is a parallel manifest for the shadcn CLI. **Duplication is +> accepted; drift is caught by PR review.** + +And from the Risks section: + +> **Drift between `packages/registry/src/components//index.tsx` (showcase) and +> `registry/base-nova/ds-/ds-.tsx` (registry).** Mitigation: drift-check script added if +> drift shows up in Phase 5. + +That mitigation shipped: `apps/web/scripts/check-registry-drift.mjs`, `docs/registry/audit-2026-07-29.json`, +and the CI `drift` job. + +## What materially changed + +The decisions log opens with *"no more re-litigation unless something materially changes the +picture."* Two things did: + +1. **Components are now authored by AI agents writing to an API, not by humans writing files.** + See `docs/reports/2026-07-30-draft-preview-admin-architecture.md`. +2. **PR review is no longer in the authoring loop.** The locked decision's stated mitigation — + *"drift is caught by PR review"* — no longer has a PR to run in. Its premise is gone, not merely + inconvenient. + +Duplication was acceptable when a human read both files before merge. With an agent writing both, +it doubles the surface where generated code can diverge silently, and the only remaining detector is +a script whose per-item assertions are hardcoded to three item names. + +## What the divergence actually was + +Measured on 2026-07-30 by reading all three files: + +| File | Content | +|---|---| +| `packages/ui/src/components/button.tsx` | Full `cva`; `cn` from `@workspace/ui/lib/utils`; exports `Button` / `buttonVariants` | +| `registry/base-nova/ds-button/ds-button.tsx` | **Identical `cva` body**; `cn` from `@/lib/utils`; exports `DsButton` / `dsButtonVariants` | +| `packages/registry/src/components/button/index.tsx` | Three-line re-export of the workspace Button, plus `ButtonDemo` | + +The `cva` bodies are byte-equivalent — same 607-character base string, same six variants, same eight +sizes. The differences are the `cn` import specifier, the export names, defaults expressed by +destructuring vs `defaultVariants`, and a named `DsButtonProps` interface on the consumer side. **For +`ds-button` there is no design divergence.** + +**`ds-button` is not representative.** Reading all nine items on 2026-07-30 rather than only the three +above: four are thin re-exports (`button`, `breadcrumb`, `tabs`, `empty`) and **five carry real +implementation** (`colored-badge`, `icon-button`, `input`, `textarea`, `blocks/empty-state`). For +`input`, `textarea`, `colored-badge` and `icon-button` there is no `packages/ui` primitive to re-export +at all — `packages/ui/src/components/` holds only five files. + +Two items genuinely diverge, and in both the consumer copy is the degraded one: `ds-icon-button` inlines +a **strict subset** of the button base string (missing `group/button`, `text-sm`, the active translate, +every `aria-invalid:*` and every `[&_svg]` rule), and `ds-colored-badge` inlines a stale `Badge` snapshot +while widening its props type with `className`. + +The drift apparatus caught neither, because its per-item assertions are hardcoded to three names while +three other items get zero assertions and the run still reports green. That — not the absence of +divergence — is the argument for deleting it. + +## The new decision + +**The two trees collapse into one.** The self-contained, consumer-facing source becomes the single +source of truth. The showcase imports it rather than maintaining a parallel version. + +Arbitration is **per item**, not blanket: + +- The consumer copy wins for `button`, `breadcrumb`, `tabs`, `empty`, `input`, `textarea` and + `blocks/empty-state`. It is the artifact that actually ships, that `contract-test.mjs` type-checks, and + that the Phase 4 sandbox validated end-to-end against the deployed registry on 2026-07-29. It is + already self-contained; the only workspace coupling is the `cn` import, which the `@/lib/utils` alias + exists to resolve. +- **The workspace copy wins for `icon-button` and `colored-badge`**, where the consumer copy is the + degraded one. Adopting it wholesale would publish a trimmed button base string as the single source of + truth — the anti-slop principle inverted. Those two implementations must be rewritten into + self-contained form before they can become the source, which is real work rather than a file move. + +Per-item measurement and the full arbitration table: `docs/reports/studio/02-single-tree.md`. + +Mechanism: a two-line bridge at `apps/web/lib/utils.ts` re-exporting `cn` from +`@workspace/ui/lib/utils`. `apps/web/tsconfig.json` already maps `@/*` to the app root. The preview +origin's import map points the same specifier at a vendored `cn`. Showcase, preview, and consumer +then execute **the same source text**. + +## What this does not change + +- **`packages/ui/src/components/button.tsx` stays.** The site chrome (`app-header`, `app-footer`, + cards, pager) imports `@workspace/ui/components/button`. That package is the showcase's own UI + kit; `ds-*` is the distributed artifact. Two legitimate consumers, two legitimate files. +- **The catalog / install-artifact split stays.** `registry.json` carries no `content`; per-item + JSONs do. Requirement #4 of the Phase 6 submission is unaffected. +- **The `Demo` stays out of the shipped artifact.** Consumers do not receive demos; the Demo becomes + a separate field alongside the source, not part of `files[]`. +- **Phases 1–5 and the Phase 6 submission entry stand as written.** + +## Consequences + +Deleted as obsolete: + +- `apps/web/scripts/check-registry-drift.mjs` +- `docs/registry/audit-2026-07-29.json` and `docs/registry/audit-2026-07-29.md` +- the CI `drift` job + +Superseded: + +- `docs/plans/2026-07-29-drift-detection.md` — its tolerance policy no longer has two trees to + tolerate a difference between. The parts describing *what counts as a meaningful divergence* + should be recycled into the write-path validators in `docs/reports/studio/agent/01-validators.md`. + +The OSS survey finding recorded in this plan — *"Nobody shares files between the showcase site and +the installable registry"* — remains accurate about those registries. It is knowingly departed from +here, because those registries are authored by humans through pull requests and this one is not. + +Full context: `docs/reports/studio/` — start at its `README.md`. diff --git a/docs/reports/2026-07-30-draft-preview-admin-architecture.md b/docs/reports/2026-07-30-draft-preview-admin-architecture.md new file mode 100644 index 0000000..2bd756d --- /dev/null +++ b/docs/reports/2026-07-30-draft-preview-admin-architecture.md @@ -0,0 +1,64 @@ +--- +title: Draft components, admin accounts, and live preview without redeploy +date: 2026-07-30 +status: superseded — decomposed into ./studio/ +--- + +# Superseded + +This report has been decomposed into **[`./studio/`](./studio/)**, one document per subsystem. + +It was a single 653-line document covering the data model, the agent contract, the preview renderer, the +publish path, the shadcn registry contract, the security model, and the changes needed in the existing +monorepo. That is seven subsystems, and keeping them in one file meant every reader loaded all of it to +answer one question. + +**Start at [`./studio/README.md`](./studio/README.md)** — it carries the problem statement, the nine locked +decisions, the topology, and an index of the rest. + +## Where each section went + +| Was | Is now | +|---|---| +| §1 the problem, §2 locked decisions, §3.1 topology | [studio/README.md](./studio/README.md) | +| §3.2 data model | [studio/01-data-model.md](./studio/01-data-model.md) | +| §4, §9 the dual tree and the `ds-button` arbitration | [studio/02-single-tree.md](./studio/02-single-tree.md) | +| §3.3 the write loop, decision 4 | [studio/agent/README.md](./studio/agent/README.md), [cli](./studio/agent/02-cli.md), [mcp](./studio/agent/03-mcp.md) | +| §6 write-path validators | [studio/agent/01-validators.md](./studio/agent/01-validators.md) | +| §5.1–5.2 transpiling and imports | [studio/preview/01-transpile-and-imports.md](./studio/preview/01-transpile-and-imports.md) | +| §5.3, §7 Tailwind `compile()` | [studio/preview/02-css-compile.md](./studio/preview/02-css-compile.md) | +| §5.4 client boundaries | [studio/preview/README.md](./studio/preview/README.md) | +| §11 security model | [studio/preview/03-security.md](./studio/preview/03-security.md) | +| §3.4 the publish loop | [studio/publish/README.md](./studio/publish/README.md) | +| the shadcn registry contract | [studio/publish/01-shadcn-registry.md](./studio/publish/01-shadcn-registry.md) | +| decision 6, the admin surface | [studio/admin/README.md](./studio/admin/README.md), [auth](./studio/admin/01-auth.md) | +| §13 Phase 0 | [studio/repo/01-decoupling.md](./studio/repo/01-decoupling.md) | +| §8, §18 Next.js versions and CVEs, §10 migration deltas | [studio/repo/02-pnpm-and-versions.md](./studio/repo/02-pnpm-and-versions.md) | +| §12 Next.js 16 mechanics | [studio/repo/03-nextjs-16.md](./studio/repo/03-nextjs-16.md) | +| §10 template reuse | [studio/repo/04-template-reuse.md](./studio/repo/04-template-reuse.md) | +| §14 frictions, §15 costs, §17 unverified | [studio/99-frictions-and-costs.md](./studio/99-frictions-and-costs.md) | +| §16 sources | distributed — each document cites its own, with verification dates | + +## What changed in the content + +The decomposition is not a copy. Every factual claim was re-measured against the repo on 2026-07-30, and +several did not survive: + +- **§4's arbitration was too broad.** "The showcase file is a re-export with no design content to lose" + holds for 4 items of 9. Five carry real implementation, and for `input`, `textarea`, `colored-badge` and + `icon-button` there is no `packages/ui` primitive to re-export at all. The arbitration is now per item, + and for `icon-button` and `colored-badge` the workspace copy wins — the consumer copies are the degraded + ones. +- **The drift apparatus was not guarding "one import specifier."** It asserts on 3 items of 9 and reports + green, while a real divergence sits in `ds-icon-button`'s base class string. +- **§5.4's "all nine items begin with `use client`"** is true of the showcase tree and false of the shipped + one, where it is 5 of 9. +- **§13's `contract-test.mjs` item list is already dynamic** (9/9). Only its npm install list is hardcoded, + and that list is currently complete. +- **§8's claim that the v16.2.6 release page lists the CVE fix** was corrected — the fix is on the v16.2.5 + page; v16.2.6 is a Turbopack follow-up. The recommendation to move to 16.2.12 is unchanged. §18 of this + report noted this but left §8 uncorrected. +- **The button cva has 8 sizes, not nine.** + +`docs/plans/2026-07-29-shadcn-registry-adoption.md` carries an addendum reversing its dual-tree decision. +That addendum repeats the first two points above and needs the same correction. diff --git a/docs/reports/studio/01-data-model.md b/docs/reports/studio/01-data-model.md new file mode 100644 index 0000000..850d9da --- /dev/null +++ b/docs/reports/studio/01-data-model.md @@ -0,0 +1,148 @@ +--- +title: Data model +date: 2026-07-30 +status: decisions locked +--- + +# Data model + +**Constrained by:** decision 2 (the database is the authoring surface), decision 5 (one tree), +decision 3 (publishing opens a PR). + +The schema is small. The single load-bearing choice is that version rows are immutable. + +--- + +## Tables + +``` +user, session, account, verification -- Better Auth, seeded with one row, signup disabled + +registry_item + id, name (ds-*), kind (component|block), category, + status (draft | published | archived), + published_version_id, head_version_id, + created_at, updated_at + +registry_item_version -- IMMUTABLE, one row per agent save + id, item_id, n, + title, description, category, variants[], + source text, -- THE component (single tree, self-contained) + demo_source text, -- the showcase Demo, never shipped to consumers + manifest jsonb, -- dependencies[], registryDependencies[], files[].target + compiled_js text, -- sucrase output, cached + compiled_css text, -- Tailwind compile output, cached + created_at, publish_commit_sha + +audit_log + action, entity, before, after, at +``` + +The Better Auth tables are **code-generated**, not hand-written — see +[repo/04-template-reuse.md](./repo/04-template-reuse.md#schema-is-generated). Adding the three tables +above means writing them alongside a generated file, not editing it. + +--- + +## `source` is singular + +Per decision 5 there is one source text per version, not a showcase copy and a consumer copy. The +file that would previously have lived at `registry/base-nova/ds-/ds-.tsx` is the whole +truth; the showcase renders that same text. [02-single-tree.md](./02-single-tree.md) covers what +"self-contained" has to mean for this to work, and the per-item arbitration that has to happen once +before it can. + +`demo_source` stays a separate column because a `Demo` is never part of a registry item's `files[]` — +consumers do not receive demos. Keeping it in its own column means the publish step never has to +strip anything out of `source`, and the validator for "does a Demo exist" checks a column rather than +parsing exports. + +--- + +## Immutability + +**Version rows are never updated.** Every agent save inserts a new row and advances +`registry_item.head_version_id`. This is the highest-leverage decision in the model, and it pays for +itself four separate ways: + +**Preview URLs become permanently cacheable.** `preview.deessejs.com/f/` describes exactly +one byte-sequence forever. `cacheLife('max')` applies with no invalidation logic, and an entire class +of stale-preview bug never exists. Without immutability the preview needs cache busting on every +save, which is precisely the loop this system is trying to make fast. + +**Compilation caches on the row.** `compiled_js` and `compiled_css` are computed once at save time +and read thereafter. See [preview/01-transpile-and-imports.md](./preview/01-transpile-and-imports.md) +and [preview/02-css-compile.md](./preview/02-css-compile.md). + +**Diff and rollback come free.** Two version rows and a text diff. No history table, no soft-delete +column, no "restore" logic — publishing an older version is setting `published_version_id`. + +**"What exactly did we publish" is answerable.** `publish_commit_sha` on the version row ties a stored +draft to a git commit. That link is the only thing connecting the database world to the static +registry world, and it survives because the row it is written on never changes. + +The cost is row count. Sources are kilobytes and an agent iterating on one component might produce +dozens of versions; that is nothing against a Neon free tier. If pruning ever matters, archived items' +non-published versions are the safe thing to drop. + +--- + +## `head_version_id` vs `published_version_id` + +Two pointers on `registry_item`, and they answer different questions: + +- `head_version_id` — what the agent last wrote. What the maintainer is looking at in Studio. +- `published_version_id` — what `ui.deessejs.com` and `/r/.json` serve. `NULL` until the first + successful publish. + +They are independent. An item can be `published` with `head_version_id` pointing at a newer draft — +that is the normal state while a revision is in review. `status` describes the item's lifecycle, not +the relationship between the two pointers, which is why `status` cannot be derived from them. + +Decision 8 reads `published_version_id`, never `head_version_id`, when resolving +`registryDependencies`. A block whose dependency has never been published has no version to resolve +against, and the preview says so rather than rendering something misleading. See +[publish/01-shadcn-registry.md](./publish/01-shadcn-registry.md#registrydependencies-resolve-to-published). + +--- + +## `manifest` + +`jsonb`, holding what `registry.json` needs per item: `dependencies[]`, `registryDependencies[]`, and +the `files[].target` value. + +Storing this as a document rather than normalized tables is deliberate — it is the shadcn schema, it +is owned upstream, and it grows new optional fields on shadcn's release schedule rather than ours. +Normalizing it would mean a migration every time shadcn adds a field. + +Two invariants on it are not free and must be enforced at write time: + +- `dependencies[]` must match the bare-module imports actually present in `source`. Today this + agreement holds across all nine items and is maintained by hand; post-decision-2 nothing maintains + it. See [agent/01-validators.md](./agent/01-validators.md). +- `registryDependencies[]` must match the `@/components/ui/*` imports in `source`. Decision 8 makes + this load-bearing: a missing entry becomes a preview render failure rather than a validation error. + +--- + +## `audit_log` + +`action, entity, before, after, at`. One writer, one reader, no roles — so this is not access control +forensics. Its job is narrower: reconstructing what an agent did across a burst of saves when +something looks wrong in the rendered output. + +Publishes and status transitions are the entries that matter. Individual `saveVersion` calls are +already recoverable from the immutable version rows, so logging them is redundant. + +--- + +## Category is a closed set + +`packages/registry/src/types.ts` defines 13 category ids (7 component, 6 block). `category` on both +the item and the version row must be one of them. `apps/web/lib/registry/index.tsx` carries +hand-maintained `CATEGORY_LABELS` and `CATEGORY_DESCRIPTIONS` maps against those 13 ids — they +currently agree, and nothing checks that they do. + +Once categories live in a database column, the closed set has to be enforced there too. This is +validator territory, not a schema constraint, because the source of truth for the list stays in +TypeScript — see [repo/01-decoupling.md](./repo/01-decoupling.md). diff --git a/docs/reports/studio/02-single-tree.md b/docs/reports/studio/02-single-tree.md new file mode 100644 index 0000000..0a9c749 --- /dev/null +++ b/docs/reports/studio/02-single-tree.md @@ -0,0 +1,216 @@ +--- +title: Collapsing the dual tree +date: 2026-07-30 +status: decisions locked +--- + +# Collapsing the dual tree + +**Constrained by:** decision 5 (the dual tree collapses into one), decision 2 (the agent writes no +files). + +**Measured against the repo:** 2026-07-30, all 9 items read in full. + +Today every registry item exists twice: once under `packages/registry/src/` (what the showcase +renders) and once under `registry/base-nova/` (what `shadcn add` ships). Decision 5 makes one of them +the source of truth and derives the other. + +The collapse itself is uncontroversial. **Which copy wins is not — and it is not the same answer for +every item.** + +--- + +## Why the duplication has to go now + +The earlier decision to accept it (`docs/plans/2026-07-29-shadcn-registry-adoption.md`) said +*"duplication is accepted; drift is caught by PR review."* Decision 2 removes the PR from the +authoring loop. The stated mitigation no longer has a place to run — its premise is gone, not merely +inconvenient. + +The fallback mitigation, `apps/web/scripts/check-registry-drift.mjs`, does not cover the gap: + +| Check | Fires for | +|---|---| +| cva equivalence (when `workspace-source-path != null`) | `ds-button`, `ds-colored-badge`, `ds-breadcrumb`, `ds-empty`, `ds-tabs` | +| `if (workspaceSrc && item.name === "ds-button")` | `ds-button` | +| `if (item.name === "ds-icon-button")` | `ds-icon-button` | +| `if (item.name === "ds-colored-badge")` | `ds-colored-badge` | + +`ds-input`, `ds-textarea` and `ds-block-empty-state` have complete entries in +`docs/registry/audit-2026-07-29.json` and hit **zero assertions** — the loop prints the item name and +passes. `registry/CONTRIBUTING.md` states the script *"fails closed for un-audited items rather than +silently skipping them"*; it fails closed on a missing **entry**, not on a missing **check**. An entry +with no matching branch passes silently. + +So the apparatus asserts on 3 of 9 items and reports green. That is the argument for deleting it — +not that there is nothing to guard. + +--- + +## What actually diverges + +Read all nine pairs, comparing every string literal byte-for-byte. + +| Item | `packages/registry/src/` copy | In sync with `base-nova`? | +|---|---|---| +| `button` | thin re-export of `@workspace/ui` Button + Demo | yes — cva base string byte-identical, 607 chars | +| `breadcrumb` | thin re-export of 7 symbols + Demo | yes — 2 of 2 strings identical | +| `tabs` | thin re-export of 4 symbols + Demo | yes — 6 of 6 strings identical | +| `empty` | re-export of 6 symbols + Demo | yes — 7 of 7 strings identical | +| `input` | **real implementation** (`forwardRef`, `displayName`, full class string) | yes — identical | +| `textarea` | **real implementation** (same shape) | yes — identical | +| `colored-badge` | **real implementation** — owns the `ColoredBadgeColor` union and an 8-entry `COLOR_CLASSES` map | **no — 8 strings vs 10** | +| `icon-button` | **real implementation** — `SIZE_CLASSES`, required `aria-label`, `Omit<…, "children" \| "size">` | **no — trimmed base string** | +| `blocks/empty-state` | **real implementation** — `EmptyStateProps`, `hasHeader` branching, composition | structurally, yes | + +Four items are thin re-exports. **Five carry real implementation**, and for `input`, `textarea`, +`colored-badge` and `icon-button` there is **no `packages/ui` primitive to re-export at all** — +`packages/ui/src/components/` contains exactly five files (`badge`, `breadcrumb`, `button`, `empty`, +`tabs`). For those four, `packages/registry` *is* the upstream. + +### `ds-icon-button` — the case that decides the rule + +The showcase version delegates to `` and therefore inherits the full +607-character button base. The `base-nova` version hand-inlines a **strict subset** of it. Missing: + +``` +group/button +text-sm +active:not-aria-[haspopup]:translate-y-px +all aria-invalid:* rules +all [&_svg] rules +``` + +It also uses `cva` for sizes where the showcase uses a plain `SIZE_CLASSES` record. The installed +component does not render identically to the showcase preview. + +### `ds-colored-badge` — self-documented copy-paste + +The `base-nova` copy adds two inlined strings absent from the workspace copy: + +``` +BADGE_BASE = "inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden + rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium …" +BADGE_OUTLINE = "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground" +``` + +The file documents this itself (lines 26–32) as a manual copy-paste, with a drift warning pointing at +`docs/plans/2026-07-29-drift-detection.md`. It renders a raw `` where the showcase renders the +`Badge` component, and `DsColoredBadgeProps` **accepts `className`** while the showcase +`ColoredBadgeProps` does not — an API divergence, not just a styling one. + +### `empty` — clean primitive, drifted demo + +The component pair is byte-identical. The showcase **Demo** hand-rolls a raw `