Skip to content

feat: component registry — spec + full implementation - #11

Open
toolpathguy wants to merge 7 commits into
mainfrom
spec/02-component-registry
Open

feat: component registry — spec + full implementation#11
toolpathguy wants to merge 7 commits into
mainfrom
spec/02-component-registry

Conversation

@toolpathguy

@toolpathguy toolpathguy commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Relates to #1

Summary

Implements the complete Component Registry — the single source of truth for all placeable building blocks in the Nuxt Visual Builder. Includes spec documents, all implementation code, and full test coverage.

What's included

Types & Validation

  • types/registry.tsRegistryEntry, Registry, and Category type definitions
  • registry/validation.ts — Zod schema with cross-field constraints (dev-only)

Pure-data entries (no .vue imports)

  • registry/entries.ts — All 13 block definitions as serializable objects

13 Block Components (app/components/blocks/)

  • 10 Nuxt UI wrappers: PageHero, PageSection, PageColumns, PageGrid, PageCTA, PageFeature, PageCard, Card, Button, Separator
  • 3 custom gap-fillers: RichText, Image, Spacer

Vue-aware registry

  • registry/index.ts — componentMap, getEntry, defaultPropsFor, entriesByCategory + dev validation guard
  • app/composables/useRegistry.ts — composable exposing registry to Vue components

Tests (19 total, all passing)

  • tests/registry.property.test.ts — 8 property-based tests (fast-check, 100 runs each)
  • tests/registry.test.ts — 7 example-based unit tests

Architecture

  • Purity split: entries.ts (pure data) vs index.ts (Vue-aware with componentMap)
  • Dev-time Zod validation: Schema enforcement at module load, tree-shaken in production
  • Object.hasOwn() guards: Prevents prototype pollution in getEntry/defaultPropsFor (caught by property tests)

What was tested

  • All 19 tests pass (npx vitest run)
  • TypeScript typecheck passes (npx nuxt typecheck)
  • Property tests validated 8 correctness properties across 100+ generated inputs each

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @toolpathguy's task in 3m 40s —— View job


Review complete: 02-component-registry spec — approve-with-nits

  • Gather context (PR diff, AI-MAP, MD Instruction 02, types/shared.ts)
  • Review requirements.md
  • Review design.md
  • Review tasks.md
  • Check consistency (versions, paths, purity, Decisions Log)
  • Post inline findings + summary verdict

Verdict: approve-with-nits. No blocking issues — the spec traces faithfully to MD Instructions/02-component-registry.md (13-block Nuxt UI set, purity split, dev-only Zod validation).

Findings posted as inline comments + a summary review comment:

  • Req 1.5 reads as a compile-time constraint but the interface only enforces it at runtime via Zod (dup of 2.5) — clarify or use a discriminated union. (requirements.md:35)
  • Req 13.6 vs 13.8 contradict on defaultPropsFor's return type; design already resolves as | undefined. (requirements.md:232)
  • Nit: fast-check assumed present but not in the tech-stack table. (design.md:462)
  • Doc drift: AI-MAP still says "8 starter blocks" and the Decisions Log still encodes the old Columns/Column model this spec replaces with PageColumns — sync recommended.

2. THE Registry_Module SHALL export a `Registry` type defined as `Record<string, RegistryEntry>` from `/types/registry.ts`
3. THE Registry_Module SHALL require the `type` field of each RegistryEntry to be a PascalCase string (matching the pattern `/^[A-Z][a-zA-Z0-9]*$/`) that is identical to its key in the Registry record
4. THE Registry_Module SHALL import `PropSchema` from `/types/shared.ts` for the `props` field definition
5. IF `acceptsChildren` is set to `false` on a RegistryEntry, THEN THE Registry_Module SHALL disallow the presence of the `allowedChildren` field on that entry

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Req 1.5 lives under "Registry Type Definitions" and says the module "SHALL disallow the presence of the allowedChildren field" when acceptsChildren is false — that reads as a compile-time guarantee. But design.md's RegistryEntry interface (lines 82–93) declares allowedChildren?: string[] as a plain optional, so { acceptsChildren: false, allowedChildren: [...] } type-checks fine. The design instead maps 1.5 to Property 1 / Zod runtime (design.md:388), which duplicates Req 2.5.

Please resolve the ambiguity: either

  • make RegistryEntry a discriminated union on acceptsChildren ({ acceptsChildren: true; allowedChildren?: string[] } | { acceptsChildren: false }) so 1.5 is enforced at the type level, or
  • reword 1.5 to state it's a runtime validation rule (making it explicitly the same guarantee as 2.5).

As written, 1.5 is not met by the designed interface.

3. WHEN the Entries_Module is imported from a plain Node.js or tsx script running outside Vite, THE Entries_Module SHALL load and evaluate without throwing errors within 2 seconds
4. THE Registry_Module at `/registry/index.ts` SHALL import entries from the Entries_Module and Vue components from `/app/components/blocks/`, and SHALL export a `registry` object re-exporting all entries keyed by type string
5. THE Registry_Module SHALL export a `componentMap` of type `Record<string, Component>` mapping each entry `type` to its Vue component, where every key in `componentMap` has a corresponding entry in the registry and every entry in the registry has a corresponding key in `componentMap`
6. THE Registry_Module SHALL export helper functions: `getEntry(type: string)` returning `RegistryEntry | undefined`, `defaultPropsFor(type: string)` returning `Record<string, unknown>`, and `entriesByCategory()` returning `Record<string, RegistryEntry[]>`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal contradiction: 13.6 states defaultPropsFor(type: string) returns Record<string, unknown>, but 13.8 (and Property 6) require it to return undefined for an unregistered type. design.md:215 correctly reconciles this as Record<string, unknown> | undefined. Update 13.6's stated return type to include | undefined so the acceptance criteria match the design.

### Test Framework & Libraries

- **Test runner**: Vitest (already configured via `vitest.config.ts`)
- **Property-based testing**: fast-check (already in devDependencies)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the testing strategy assumes fast-check is "already in devDependencies", but it isn't listed in the AI-MAP tech-stack table (Testing | Vitest, @vue/test-utils, @nuxt/test-utils, happy-dom). The property tests (task 8.x) can't run without it. Either add fast-check to Task 01's scope / the tech-stack table, or note it as a dependency this task must add. (The 8.x tests are marked optional *, so this is non-blocking.)

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review: 02-component-registry spec — approve-with-nits

Spec-only PR (requirements + design + tasks). It traces faithfully to MD Instructions/02-component-registry.md — the 13-block Nuxt-UI-primary set, the purity split, compileAs/U* convention, and dev-only Zod validation all line up. The purity split is well-specified (Req 13, Property 7's JSON.parse(JSON.stringify) check is a nice serializable-data guarantee), and the property/requirement traceability table in design.md is thorough.

No blocking issues. A few things to resolve before or during implementation:

Should fix (spec clarity / traceability)

  • Req 1.5 is unmet by the designed interface. It reads as a compile-time "disallow allowedChildren when acceptsChildren: false", but the RegistryEntry interface uses a plain optional and the design only enforces this at runtime via Zod (dup of Req 2.5). Either make RegistryEntry a discriminated union on acceptsChildren, or reword 1.5 as a runtime rule. (inline on requirements.md:35)
  • Req 13.6 vs 13.8 contradiction: 13.6 says defaultPropsFor returns Record<string, unknown>; 13.8/Property 6 require undefined for unknown types. design.md:215 already uses | undefined — align 13.6 to match. (inline on requirements.md:232)

Nits

  • fast-check is assumed "already in devDependencies" (design.md:462) but isn't in the AI-MAP tech-stack table; ensure Task 01 adds it (property tests are optional *, so non-blocking). (inline on design.md:462)
  • Doc drift (not introduced by this PR, but worth syncing): AI-MAP.md still notes "Registry types, 8 starter blocks" for Task 02 (line 60), and the Decisions Log still records "Columns only accepts Column children" + Column.span (line 147) — a Column/Columns model this spec replaces with PageColumns (no allowedChildren, no Column block). Per the Decisions Log's own "keep docs in sync" rule, AI-MAP should be updated to reflect the Nuxt UI pivot so later tasks (03 validateAgainstRegistry, 07 drop-guard) don't re-derive the stale Column constraint. Not relitigating the pivot itself.

CI: ci job passing; review pending. This is a code/spec-level review only.

@toolpathguy
toolpathguy marked this pull request as draft July 23, 2026 00:12
@toolpathguy
toolpathguy marked this pull request as ready for review July 23, 2026 00:12
@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @toolpathguy's task in 2m 46s —— View job


Review: 02-component-registry spec — approve-with-nits

  • Read PR diff (requirements / design / tasks)
  • Read AI-MAP.md (conventions + Decisions Log)
  • Read MD Instruction 02 + types/shared.ts
  • Check spec traceability + internal consistency
  • Post inline finding + summary verdict

Verdict: approve-with-nits. All three prior inline nits are resolved in ce07adc (Req 1.5 reworded as runtime rule, Req 13.6 return type aligned, fast-check now a declared task dependency). Spec traces faithfully to MD Instructions/02-component-registry.md.

One new should-fix and two nits:

  • Should fix: validateRegistry (design.md:148) joins only i.message, dropping i.path — so error messages won't name the failing prop key / label field that Req 2.2 & 2.3 require. Property 1 ("type or key") is already met by the wrapper, so it wouldn't catch this. Inline suggestion posted.
  • Nit: AI-MAP doc drift (still "8 starter blocks" + stale Columns/Column model) — follow-up, out of scope for a spec-only PR.
  • Nit: verify Zod-4 API (z.ZodIssueCode.custom, z.ZodType widening) at implementation time.

Full write-up in the summary comment. CI (ci + review) pending at review time — code-level review only.

for (const [key, entry] of Object.entries(registry)) {
const result = registryEntryZod.safeParse(entry)
if (!result.success) {
const issues = result.error.issues.map(i => i.message).join('; ')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix (spec-level, affects traceability): this sketch joins only i.message and discards i.path, but several acceptance criteria require the error to identify which field failed:

  • Req 2.3 — "error that includes the entry's type and the invalid prop key name". The prop key lives only in i.path (['props', '<key>', 'type']); with a discriminatedUnion on type, the message alone won't name the prop.
  • Req 2.2 — "indicates the label is missing or empty". z.string().min(1) yields a generic "String must contain at least 1 character(s)" with path: ['label']; the message alone doesn't say "label".
  • Req 2.9 — "the invalid category value" (Zod's enum message usually includes the received value, so this one is likely OK).

Recommend folding the path into the message, e.g.:

Suggested change
const issues = result.error.issues.map(i => i.message).join('; ')
const issues = result.error.issues
.map(i => `${i.path.join('.') || '<root>'}: ${i.message}`)
.join('; ')

Related: Property 1 (design.md:386) only asserts the message "includes the entry's type or registry key" — the wrapping Registry validation failed for "${key}" already guarantees that regardless of i.path. So the Property-1 test would pass even while Req 2.3's "invalid prop key name" guarantee is unmet. Consider strengthening Property 1 (or a 9.1 unit test) to assert the failing field/prop key appears, so the criterion is actually guarded.

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Re-review: `02-component-registry` spec — approve-with-nits

Spec-only PR (requirements + design + tasks). It traces faithfully to `MD Instructions/02-component-registry.md`: the 13-block Nuxt-UI-primary set, the `compileAs`/`U*` convention, dev-only Zod validation, and the purity split all line up. Genuinely nice touches: the requirement↔property traceability table, Property 7's `JSON.parse(JSON.stringify)` serializability guarantee for the pure-data layer, and the discriminated-union PropSchema reuse from `/types/shared.ts`.

Prior review feedback — resolved ✅

  • Req 1.5 reworded as an explicit runtime Zod rule (same guarantee as 2.5) — no longer reads as an unmet compile-time constraint.
  • Req 13.6 return type now `Record<string, unknown> | undefined`, aligned with 13.8 / Property 6 / design.md:215.
  • fast-check now called out as a devDependency the tasks must add (design.md:462, task 8.0).

Should fix (spec clarity → implementation traceability)

  • Error messages drop `i.path`. `validateRegistry` (design.md:148) joins only `i.message`, so the thrown error won't name the offending field. Req 2.3 requires "the invalid prop key name" and Req 2.2 requires it to "indicate the label is missing or empty" — neither is met by `message` alone (the prop key/field lives in `i.path`). Suggested one-line fix inline. Also note Property 1 as written ("type or key") is already satisfied by the wrapper string, so the property test would pass even with Req 2.3 unmet — worth strengthening. (inline on design.md:148)

Nits (non-blocking)

  • Doc drift, still unsynced. `AI-MAP.md` line 61 still says Task 02 = "8 starter blocks" (spec is 13), line 127 still states "Columns block: only accepts Column children", and the Decisions Log (line 147) still encodes the `Columns`/`Column` + `Column.span` model this spec supersedes with `PageColumns` (no `allowedChildren`, no `Column` block). Not relitigating the Nuxt UI pivot — just flagging that per the Decisions Log's own "keep docs in sync" rule, AI-MAP should be updated so later tasks (03 `validateAgainstRegistry`, 07 drop-guard) don't re-derive the stale Column constraint. Out of scope for a spec-only PR, but worth a follow-up. (Not editable inline — AI-MAP isn't in this diff.)
  • Verify the Zod-4 API in the design sketch during implementation: `z.ZodIssueCode.custom` changed in Zod v4 (`code: 'custom'` string literal is the v4 form), and `propSchemaZod: z.ZodType` widens away the discriminated typing. Both are code sketches, so non-blocking. (design.md:104, 130)

CI: `ci` + `review` jobs still pending at review time — this is a spec/code-level review only.

@toolpathguy
toolpathguy marked this pull request as draft July 23, 2026 00:18
@toolpathguy
toolpathguy marked this pull request as ready for review July 23, 2026 00:18
@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @toolpathguy's task in 2m 23s —— View job


Re-review: 02-component-registry spec — approve

  • Read latest diff (post-9935878)
  • Verify prior should-fix (Zod i.path in errors) resolved
  • Check spec traceability + internal consistency (requirements ↔ design ↔ tasks, /types/shared.ts)
  • Post verdict

Spec-only PR (requirements + design + tasks). It traces faithfully to MD Instructions/02-component-registry.md and stays internally consistent: the 13-block Nuxt-UI-primary set, the compileAs/U* convention, dev-only Zod validation, and the purity split all line up. The propSchemaZod discriminated union matches /types/shared.ts PropSchema variant-for-variant (all 8 types + optional label), categories in the design registry table match each block's requirement, and the requirement↔property traceability table is complete.

All prior review feedback — resolved ✅

  • Zod i.path droppedvalidateRegistry now emits ${i.path.join('.') || '<root>'}: ${i.message} (design.md:148–149), so errors name the failing prop key / label field. Req 2.2 (label), 2.3 (invalid prop key), and 2.9 (category) are now satisfiable by the message.
  • Property 1 strengthened (design.md:388) → now asserts type or key AND the path to the failing field, so the property test actually guards Req 2.3's "invalid prop key name" rather than passing on the wrapper string alone.
  • Req 1.5 reworded as an explicit runtime Zod rule (same guarantee as 2.5).
  • Req 13.6 return type aligned to Record<string, unknown> | undefined.
  • fast-check declared as a devDependency the tasks must add (design.md:464, task 8.0).

Nits (non-blocking, implementation-time)

  • RichText v-html is an XSS sink if body ever comes from anything but the trusted page author. Req 12.6 mandates HTML rendering, so v-html is intended — just decide explicitly (trusted-author-only vs. sanitize) at implementation. Inline on design.md:288.
  • Zod v4 API check at implementation: z.ZodIssueCode.custom (v4 prefers code: 'custom') and propSchemaZod: z.ZodType widens away the discriminated typing. Both are code sketches — verify against the pinned Zod version.
  • AI-MAP doc drift (out of scope — not in this diff, follow-up): AI-MAP.md line 61 still says Task 02 = "8 starter blocks" (spec is 13), and lines 127/147 still encode the Columns/Column + Column.span model this spec supersedes with PageColumns (no allowedChildren, no Column block). Per the Decisions Log's own "keep docs in sync" rule, AI-MAP should be updated so later tasks (03 validateAgainstRegistry, 07 drop-guard) don't re-derive the stale Column constraint. Not relitigating the Nuxt UI pivot.

Genuinely nice: Property 7's JSON.parse(JSON.stringify) serializability guarantee for the pure-data layer, and the purity-split mermaid diagram making the "no .vue transitive imports" boundary explicit.

CI: only the Claude PR Review job is reporting (in progress) — this is a spec/code-level review only; no lint/typecheck/build gate applies to a docs-only diff.

```vue
<!-- BlockRichText.vue -->
<template>
<div :class="['prose max-w-none', alignClass]" v-html="body" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (implementation-time, security): BlockRichText renders the body prop with v-html. Req 12.6 does mandate "render the body prop value as HTML content," so v-html is the intended mechanism — but the compiled output is a published Nuxt site, and unsanitized v-html is a stored-XSS sink if body ever originates from anything but the page author's own trusted input. Worth deciding explicitly at implementation: either document that RichText content is trusted-author-only, or sanitize (e.g. via a sanitizer) before binding. Non-blocking for a spec PR.

@toolpathguy
toolpathguy marked this pull request as draft July 23, 2026 00:52
- Add RegistryEntry/Registry types in /types/registry.ts
- Add Zod validation module with cross-field constraints
- Add pure-data entries module (13 blocks, zero .vue imports)
- Add 13 block components (10 Nuxt UI wrappers + 3 custom gap-fillers)
- Add Vue-aware registry module with componentMap and helpers
- Add useRegistry() composable
- Add 8 property-based tests and 7 unit tests (all passing)
- Fix prototype pollution in getEntry/defaultPropsFor with Object.hasOwn()
@toolpathguy toolpathguy changed the title feat: 02-component-registry spec (requirements, design, tasks) feat: component registry — spec + full implementation Jul 23, 2026
@toolpathguy
toolpathguy marked this pull request as ready for review July 23, 2026 00:54
Repository owner deleted a comment from claude Bot Jul 23, 2026
Repository owner deleted a comment from claude Bot Jul 23, 2026
@toolpathguy
toolpathguy marked this pull request as draft July 23, 2026 01:00
@toolpathguy
toolpathguy marked this pull request as ready for review July 23, 2026 01:00
@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude encountered an error after 2m 50s —— View job


Reviewing implementation: 02-component-registry

  • Read implementation diff (types, registry, blocks, tests)
  • Check registry purity (no .vue transitive imports in entries.ts / index.ts split)
  • Verify spec traceability (13 blocks, validation, defaultPropsFor)
  • Review types (no any), correctness, security (v-html)
  • Check tests assert what they claim
  • Post inline findings + summary verdict

Repository owner deleted a comment from claude Bot Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant