From d30416a10695fbe736e9ee707f2089dab0177bc3 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Tue, 21 Jul 2026 09:17:18 +0000 Subject: [PATCH 1/3] docs: chart json-render package wayfinder --- .../01-research-upstream-json-render.md | 15 ++ .../02-research-existing-integration-seams.md | 15 ++ .../issues/03-define-package-contracts.md | 13 ++ .../issues/04-define-protocol-semantics.md | 13 ++ .../05-define-devframe-runtime-integration.md | 13 ++ .../issues/06-define-hub-projection.md | 13 ++ .../issues/07-define-official-vue-ui.md | 13 ++ .../issues/08-prototype-consumer-api.md | 13 ++ .../issues/09-assemble-implementation-spec.md | 13 ++ plans/json-render/map.md | 33 ++++ .../research/existing-integration-seams.md | 151 ++++++++++++++++++ .../research/upstream-json-render.md | 88 ++++++++++ 12 files changed, 393 insertions(+) create mode 100644 plans/json-render/issues/01-research-upstream-json-render.md create mode 100644 plans/json-render/issues/02-research-existing-integration-seams.md create mode 100644 plans/json-render/issues/03-define-package-contracts.md create mode 100644 plans/json-render/issues/04-define-protocol-semantics.md create mode 100644 plans/json-render/issues/05-define-devframe-runtime-integration.md create mode 100644 plans/json-render/issues/06-define-hub-projection.md create mode 100644 plans/json-render/issues/07-define-official-vue-ui.md create mode 100644 plans/json-render/issues/08-prototype-consumer-api.md create mode 100644 plans/json-render/issues/09-assemble-implementation-spec.md create mode 100644 plans/json-render/map.md create mode 100644 plans/json-render/research/existing-integration-seams.md create mode 100644 plans/json-render/research/upstream-json-render.md diff --git a/plans/json-render/issues/01-research-upstream-json-render.md b/plans/json-render/issues/01-research-upstream-json-render.md new file mode 100644 index 00000000..a2a74adc --- /dev/null +++ b/plans/json-render/issues/01-research-upstream-json-render.md @@ -0,0 +1,15 @@ +# Research upstream json-render contracts + +Type: research +Status: resolved +Blocked by: none + +## Question + +Which public contracts and runtime semantics in the current `@json-render/core` and `@json-render/vue` releases must the Devframes packages adopt, re-export, configure, or deliberately constrain? Compare the current release with the version embedded by `vitejs/devtools`, covering schema/catalog typing, registries, state and binding expressions, actions and validation, streaming, extension composition, renderer lifecycle, package stability, and licensing. + +## Answer + +See [Upstream json-render contracts](../research/upstream-json-render.md). + +The current upstream pair is `@json-render/core@0.19.0` and `@json-render/vue@0.19.0`. Its catalog and registry types aid authoring but do not fully validate component props, authorize actions, compose extensions, or guarantee lifecycle behavior. A direct Devframes re-export therefore needs reviewed named exports, exact upstream pinning, a separately versioned protocol validator, and compatibility tests. The report also records the Apache-2.0 obligations and recommends keeping streaming optional until the protocol decision explicitly adopts it. diff --git a/plans/json-render/issues/02-research-existing-integration-seams.md b/plans/json-render/issues/02-research-existing-integration-seams.md new file mode 100644 index 00000000..1332def3 --- /dev/null +++ b/plans/json-render/issues/02-research-existing-integration-seams.md @@ -0,0 +1,15 @@ +# Inventory existing JSON-render and runtime seams + +Type: research +Status: resolved +Blocked by: none + +## Question + +What behavior currently exists in this repository and `vitejs/devtools`, and which devframe and hub seams can host it without violating their boundaries? Trace the current hub types, `createJsonRenderer`, shared-state transport, dock projection, Vite viewer, standalone adapters, static dumps, client host, package build conventions, and tests. Record concrete compatibility obligations and architectural constraints rather than proposing the final API. + +## Answer + +See [Existing JSON-render integration seams](../research/existing-integration-seams.md). + +The current repository owns permissive hub types, a hub-only shared-state factory, and a JSON-render dock discriminator; `vitejs/devtools` owns the only renderer and catalog. Wire projection accidentally strips renderer methods and leaves `_stateKey`, standalone adapters have no renderer assets or factory, and static dumps can replay specs but not RPC actions. The hub's existing headless client boundary is suitable for renderer substitution, while stable scoped identities, explicit serialization, cleanup, standalone assets, declared-action isolation, and dedicated tests are missing. diff --git a/plans/json-render/issues/03-define-package-contracts.md b/plans/json-render/issues/03-define-package-contracts.md new file mode 100644 index 00000000..eb6da10e --- /dev/null +++ b/plans/json-render/issues/03-define-package-contracts.md @@ -0,0 +1,13 @@ +# Define package ownership and public exports + +Type: grilling +Status: open +Blocked by: 01, 02 + +## Question + +What does each package own and export, and what dependency direction keeps the protocol stable, devframe-native, and renderer-swappable? Resolve the direct `@json-render/core` re-export surface, schema and catalog exports, renderer handles, Vue registry and shell exports, subpath exports, peer dependencies, type names, and the boundary with `devframe` and `@devframes/hub`. + +## Answer + +Pending. diff --git a/plans/json-render/issues/04-define-protocol-semantics.md b/plans/json-render/issues/04-define-protocol-semantics.md new file mode 100644 index 00000000..0eb1fd4d --- /dev/null +++ b/plans/json-render/issues/04-define-protocol-semantics.md @@ -0,0 +1,13 @@ +# Define catalog, state, and action semantics + +Type: grilling +Status: open +Blocked by: 01, 03 + +## Question + +What makes a Devframes JSON-render spec valid and portable? Define the versioned base catalog, extension catalogs and compatibility metadata, declared and validated actions, state ownership and binding updates, replacement versus incremental updates, whether streaming/generative-UI primitives enter the initial protocol, serialization limits, validation timing, failure behavior, and how direct upstream re-exports coexist with a stable Devframes protocol. + +## Answer + +Pending. diff --git a/plans/json-render/issues/05-define-devframe-runtime-integration.md b/plans/json-render/issues/05-define-devframe-runtime-integration.md new file mode 100644 index 00000000..e5994ea8 --- /dev/null +++ b/plans/json-render/issues/05-define-devframe-runtime-integration.md @@ -0,0 +1,13 @@ +# Define devframe runtime integration + +Type: grilling +Status: open +Blocked by: 02, 03, 04 + +## Question + +How does an author declare and update one or more JSON-render views on a devframe, and how does the same definition run through CLI, dev, SPA, build, Vite, and embedded adapters? Resolve view identity and lifecycle, scoped shared state and RPC actions, static snapshot behavior, default standalone UI assets, ownership of `createJsonRenderer`, disposal, reconnects, and whether the integration requires a general devframe view primitive. + +## Answer + +Pending. diff --git a/plans/json-render/issues/06-define-hub-projection.md b/plans/json-render/issues/06-define-hub-projection.md new file mode 100644 index 00000000..57ad2f08 --- /dev/null +++ b/plans/json-render/issues/06-define-hub-projection.md @@ -0,0 +1,13 @@ +# Define hub projection and renderer substitution + +Type: grilling +Status: open +Blocked by: 03, 04, 05 + +## Question + +How does a hub discover and project a devframe JSON-render view into a dock while remaining headless and UI-framework neutral? Define dock metadata, client-host responsibilities, renderer selection and injection, use of the official Vue UI versus a third-party implementation, catalog compatibility checks, loading and error states, disposal, and migration of the current hub-owned JSON-render types and factory. + +## Answer + +Pending. diff --git a/plans/json-render/issues/07-define-official-vue-ui.md b/plans/json-render/issues/07-define-official-vue-ui.md new file mode 100644 index 00000000..6f3b1c1d --- /dev/null +++ b/plans/json-render/issues/07-define-official-vue-ui.md @@ -0,0 +1,13 @@ +# Define the official Vue and Antfu Design UI + +Type: grilling +Status: open +Blocked by: 04 + +## Question + +What exactly does `@devframes/json-render-ui` ship as the opinionated Vue implementation? Resolve its base component registry, fidelity and intentional changes from the Vite DevTools catalog, `@antfu/design` component mapping, semantic tokens and UnoCSS integration, icons, accessibility, loading and error surfaces, registry extension, renderer shell APIs, static-app assets, Storybook coverage, and theming responsibilities. + +## Answer + +Pending. diff --git a/plans/json-render/issues/08-prototype-consumer-api.md b/plans/json-render/issues/08-prototype-consumer-api.md new file mode 100644 index 00000000..1cfdd5f7 --- /dev/null +++ b/plans/json-render/issues/08-prototype-consumer-api.md @@ -0,0 +1,13 @@ +# Prototype the end-to-end consumer API + +Type: prototype +Status: open +Blocked by: 05, 06, 07 + +## Question + +Does one small, disposable API prototype make the intended seams natural for all key consumers? Demonstrate one view authored once, rendered by the official Vue registry standalone and in a hub dock, replaced by a third-party registry, updated through declared live actions and state, and emitted as a static snapshot. Use the prototype to resolve awkward ownership, naming, lifecycle, and configuration before the specification is assembled. + +## Answer + +Pending. diff --git a/plans/json-render/issues/09-assemble-implementation-spec.md b/plans/json-render/issues/09-assemble-implementation-spec.md new file mode 100644 index 00000000..95140e20 --- /dev/null +++ b/plans/json-render/issues/09-assemble-implementation-spec.md @@ -0,0 +1,13 @@ +# Assemble the implementation-ready specification + +Type: task +Status: open +Blocked by: 01, 02, 03, 04, 05, 06, 07, 08 + +## Question + +Consolidate the resolved decisions and prototype into the destination specification without reopening them. Include package manifests and exports, public TypeScript API sketches, runtime and data flows, component catalog, extension and substitution model, devframe and hub changes, migration of existing repository APIs, diagnostics, documentation, test matrix, acceptance criteria, and an ordered implementation sequence suitable for follow-up tickets. + +## Answer + +Pending. diff --git a/plans/json-render/map.md b/plans/json-render/map.md new file mode 100644 index 00000000..818b218b --- /dev/null +++ b/plans/json-render/map.md @@ -0,0 +1,33 @@ +# JSON Render Packages Wayfinder + +## Destination + +Produce an implementation-ready specification for `@devframes/json-render` and `@devframes/json-render-ui`: their package boundaries, public APIs, protocol semantics, official Vue renderer, devframe runtime support, hub integration, migration inside this repository, acceptance criteria, and implementation sequence. + +## Notes + +- This map plans the work; implementing or releasing the packages is outside its destination. +- `@devframes/json-render` is a stable, devframe-native protocol package. It directly re-exports the relevant `@json-render/core` API and owns the Devframes catalog, declared-action contract, runtime handles, and integration helpers. +- `@devframes/json-render-ui` is the official Vue implementation. Its registry implements the catalog with `@antfu/design`, while third-party registries can replace it. +- The catalog has a versioned portable base and explicit extensions. A registry is a framework-specific implementation of a catalog. +- Views work in standalone and hub deployments across live and static devframe outputs. Live outputs can use RPC actions; static behavior must be specified explicitly. +- Specs invoke only actions declared for that view. An arbitrary string-to-RPC bridge is not part of the target contract. +- The final specification covers this repository. Adoption and code removal in `vitejs/devtools` is a separate effort. +- Relevant skills: `research`, `grilling`, `domain-modeling`, `prototype`, `antfu`, `antfu-design`, `vue-best-practices`, `unocss`, `vite`, `tsdown`, and `vitest`. +- Local tracker operations: tickets are files under `issues/`; `Status: open` is unclaimed, `Status: claimed` is in progress, and `Status: resolved` is closed. A ticket is on the frontier when every number in `Blocked by` is resolved. + +## Decisions so far + +- [Research upstream json-render contracts](issues/01-research-upstream-json-render.md) — Upstream `0.19.0` is structurally reusable but pre-1.0 typing is not a validation or authorization boundary; direct re-exports require pinning and a Devframes-owned protocol guard. +- [Inventory existing JSON-render and runtime seams](issues/02-research-existing-integration-seams.md) — The current hub transports whole specs through shared state and an accidental `_stateKey` projection; portable standalone/static rendering, stable identity, action isolation, cleanup, and tests remain to be designed. + +## Not yet specified + +- The implementation slices and test matrix. These become precise after the public API prototype establishes the package seams. + +## Out of scope + +- Implementing, publishing, or releasing the packages. +- Changing `vitejs/devtools` to consume the packages or deleting its current implementation. +- Official React, Solid, Svelte, or vanilla registries; third-party renderer compatibility is specified through the core contract. +- Agent-driven UI generation beyond any protocol capability required to keep the base catalog compatible with json-render. diff --git a/plans/json-render/research/existing-integration-seams.md b/plans/json-render/research/existing-integration-seams.md new file mode 100644 index 00000000..291eb97e --- /dev/null +++ b/plans/json-render/research/existing-integration-seams.md @@ -0,0 +1,151 @@ +# Existing JSON-render integration seams + +Research snapshot: local checkout `d31b345b18bf66a98ec4cc87ff1a896232e94f7e`; upstream `vitejs/devtools` commit [`1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360`](https://github.com/vitejs/devtools/commit/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360) (`v0.4.2`, 2026-07-21). + +## Summary + +The repository already has a small hub-owned transport contract: a permissive spec type, a server handle backed by devframe shared state, and a `json-render` dock discriminator. It does not contain a renderer or catalog. `vitejs/devtools` supplies the only current viewer: a Vue shell around `@json-render/vue`, a 14-component catalog/registry, and an unconstrained action-name-to-RPC proxy. + +This split works in the Vite hub because the viewer needs only the handle's `_stateKey`. It is not yet a portable devframe feature: standalone dev/build adapters construct the base `DevframeNodeContext`, have no `createJsonRenderer`, and copy an author-provided SPA rather than shipping a JSON-render viewer. Static RPC machinery can snapshot shared-state values, but action/event RPC is intentionally absent from static dumps. + +## Current repository + +### Hub contract and factory + +- [`packages/hub/src/types/json-render.ts`](../../../packages/hub/src/types/json-render.ts) defines handwritten, dependency-free `JsonRenderElement`, `JsonRenderSpec`, and `JsonRenderer` types. An element has `type`, optional `props`, `children`, `on`, `visible`, and `repeat`, plus an open index signature. A spec has `root`, `elements`, and optional `state`. There is no catalog generic, schema validation, action declaration, renderer lifecycle, or upstream `@json-render/core` type identity. +- [`packages/hub/src/define.ts`](../../../packages/hub/src/define.ts) exposes `defineJsonRenderSpec` as an identity helper. The types and helper are public through `@devframes/hub` and `@devframes/hub/types`; the current API snapshot records them in [`tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts`](../../../tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts). +- [`packages/hub/src/node/context.ts`](../../../packages/hub/src/node/context.ts) adds `createJsonRenderer` only to `DevframeHubContext`. Each call allocates `devframe:json-render:`, creates a shared state whose value is the complete spec, and returns: + - `_stateKey`, the actual cross-process locator; + - `updateSpec`, which replaces the complete shared value; + - `updateState`, which shallow-merges keys into `spec.state`. + Keys are allocation-order based, unscoped, and have no stable view ID. Handles have no disposal or listener cleanup, and state entries live for the context lifetime. +- The base context deliberately lacks this factory. [`packages/devframe/src/node/context.ts`](../../../packages/devframe/src/node/context.ts) creates framework-neutral RPC, views, diagnostics, agent, services, and scope surfaces; its comment names JSON rendering as a host augmentation. [`packages/devframe/src/node/scope.ts`](../../../packages/devframe/src/node/scope.ts) namespaces RPC/shared-state/streaming for a tool, but hub additions including `createJsonRenderer` are not carried onto the scoped context. + +### Dock projection + +- [`packages/hub/src/types/docks.ts`](../../../packages/hub/src/types/docks.ts) defines a `DevframeViewJsonRender` as `{ type: 'json-render', ui: JsonRenderer }` and includes it in both user and projected dock unions. +- [`packages/hub/src/node/host-docks.ts`](../../../packages/hub/src/node/host-docks.ts) stores the original entry. `values()` performs special projection only for remote iframes; it does not explicitly reduce a JSON-render handle to a serializable reference. +- [`packages/hub/src/node/context.ts`](../../../packages/hub/src/node/context.ts) mirrors `docks.values()` into `devframe:docks` (10 ms debounce in dev, 0 ms in build). Therefore in-process consumers see the methods, while wire/static serialization removes function-valued object properties. Devframe's default structured-clone serializer uses `structured-clone-es.stringify`; that function uses JSON/lossy mode, which skips object properties containing functions ([implementation](https://unpkg.com/structured-clone-es@2.0.0/dist/index.mjs), [documented lossy behavior](https://unpkg.com/structured-clone-es@2.0.0/README.md)). The effective client payload is the dock metadata plus `ui._stateKey`, not a callable `JsonRenderer`. +- The public projected dock type consequently overstates what an out-of-process client receives. The Vite viewer is compatible because it reads only `_stateKey`; any extracted viewer must retain a serializable locator or deliberately replace this accidental projection contract. + +### Shared-state transport + +The complete path is: + +1. The hub factory registers the spec under its generated key. +2. A dock entry carrying that handle is mirrored into `devframe:docks`. +3. A client reads the dock list, extracts `_stateKey`, and asks for that shared state. +4. Live server mutations broadcast full snapshots to subscribed sessions; the viewer replaces its spec reference. + +The mechanics are in: + +- [`packages/devframe/src/utils/shared-state.ts`](../../../packages/devframe/src/utils/shared-state.ts): immutable reads, Immer-backed `mutate`, optional patches, `updated` events, and bounded sync-ID de-duplication. +- [`packages/devframe/src/node/rpc-shared-state.ts`](../../../packages/devframe/src/node/rpc-shared-state.ts): state registry; subscribe/get/set/patch RPC declarations; subscriber-filtered broadcasts. Server-created RPC shared states currently disable patches, so JSON-render updates travel as complete snapshots even when only `spec.state` changes. +- [`packages/devframe/src/client/rpc-shared-state.ts`](../../../packages/devframe/src/client/rpc-shared-state.ts): trust-gated subscription and initial fetch, local state cache, full-state/patch application, and loop prevention. Client writes are suppressed for the static backend. +- [`packages/devframe/src/rpc/serialization.ts`](../../../packages/devframe/src/rpc/serialization.ts): strict JSON only for RPC declarations marked `jsonSerializable: true`; structured-clone encoding otherwise. JSON-render specs are expected to be serializable by design, but the current shared-state getter is not marked JSON-only and therefore does not validate that obligation. + +There is no client-to-server synchronization of the renderer provider's internal `$state` merely because it began in `spec.state`. Bound values can be resolved into action parameters by `@json-render/vue`; server visibility requires an RPC action or a separate shared-state write. + +### Headless client host + +[`packages/hub/src/client/host.ts`](../../../packages/hub/src/client/host.ts) is framework-neutral orchestration, not a viewer. `createDevframeClientHost` connects RPC, subscribes to docks/commands/settings, builds panel/dock/command/when contexts, publishes them on `globalThis`, handles dock activation, and dynamically imports scripts for `action`, `custom-render`, and iframe `clientScript` entries. It neither recognizes nor renders `json-render` entries specially. See also [`packages/hub/src/client/docks.ts`](../../../packages/hub/src/client/docks.ts) and [`packages/hub/src/client/context.ts`](../../../packages/hub/src/client/context.ts). + +This is the correct headless boundary: a viewer may consume the shared dock metadata and RPC context, while the hub package itself must not acquire Vue, a component registry, or presentation policy. + +### Standalone, hosted, live, and static adapters + +- [`packages/devframe/src/adapters/dev.ts`](../../../packages/devframe/src/adapters/dev.ts) creates a base context, runs `definition.setup`, serves an author SPA when `cli.distDir` exists, and exposes live WebSocket RPC. It does not create a hub context or supply a JSON-render UI. +- [`packages/devframe/src/adapters/build.ts`](../../../packages/devframe/src/adapters/build.ts) likewise creates a base build context, runs setup, copies an author SPA verbatim, and writes connection metadata plus RPC dump shards. It has no default renderer assets and cannot directly satisfy a definition that expects `createJsonRenderer`. +- [`packages/devframe/src/adapters/embedded.ts`](../../../packages/devframe/src/adapters/embedded.ts) only runs a definition against a caller-provided context. A hub context can therefore expose the existing factory at runtime, but the definition type remains the base `DevframeNodeContext` and the adapter supplies no viewer. +- [`packages/devframe/src/helpers/vite.ts`](../../../packages/devframe/src/helpers/vite.ts) either mounts static SPA assets or starts the same base standalone dev server as a sidecar. This helper is distinct from the hub-aware Vite DevTools kit. +- [`packages/hub/src/node/mount-devframe.ts`](../../../packages/hub/src/node/mount-devframe.ts) is the hosted hub seam. It serves a devframe's SPA, synthesizes an iframe dock, and runs setup on the hub context. It does not synthesize a JSON-render view from a devframe definition. +- Mount defaults are `/` for standalone and `/__/` for hosted adapters ([`packages/devframe/src/adapters/_shared.ts`](../../../packages/devframe/src/adapters/_shared.ts)). Browser connection discovery is relative to the executing SPA/dump base ([`packages/devframe/src/client/rpc.ts`](../../../packages/devframe/src/client/rpc.ts)). Any standalone JSON-render viewer must preserve this runtime base behavior. +- The type vocabulary includes `spa`, but this checkout has no `createSpa` implementation or `devframe/adapters/spa` export. Current executable adapters are the package exports listed in [`packages/devframe/package.json`](../../../packages/devframe/package.json), and `createCac` currently wires `dev`, `build`, and `mcp` in [`packages/devframe/src/adapters/cac.ts`](../../../packages/devframe/src/adapters/cac.ts). + +### Static dump behavior + +- `devframe:rpc:server-state:get` declares dump inputs for every registered shared-state key ([`packages/devframe/src/node/rpc-shared-state.ts`](../../../packages/devframe/src/node/rpc-shared-state.ts)). If a build uses a hub context and all renderer/dock states exist before dump collection, both `devframe:docks` and each generated JSON-render state are eligible for snapshots. +- [`packages/devframe/src/rpc/dump/static.ts`](../../../packages/devframe/src/rpc/dump/static.ts) dumps only `static` and configured `query` RPC. It shards query records by argument hash and chooses strict JSON or structured clone from the RPC declaration. `createBuild` writes the manifest/shards and `{ backend: 'static' }` metadata. +- [`packages/devframe/src/client/rpc-static.ts`](../../../packages/devframe/src/client/rpc-static.ts) and [`packages/devframe/src/client/static-rpc.ts`](../../../packages/devframe/src/client/static-rpc.ts) replay those reads. Events are no-ops; missing ordinary calls fail. +- Consequently, a snapshotted JSON-render spec can render read-only, and local input bindings may still work inside the renderer. RPC `action`/`event` declarations are not dumped, so the current action bridge cannot perform server mutations in static output. Static behavior must be specified rather than inferred from the live Vite implementation. +- Build-time ordering matters: shared-state keys must be registered before `collectStaticRpcDump` enumerates them, and dock mirroring is debounced even though build mode uses a zero delay. This path currently has no JSON-render-specific regression test. + +## `vitejs/devtools` primary implementation + +All links in this section are pinned to commit `1b13d847`. + +### Ownership and dependency version + +The kit now re-exports JSON-render and dock types from `@devframes/hub` rather than implementing them ([`packages/kit/src/types/json-render.ts`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/kit/src/types/json-render.ts), [`packages/kit/src/types/docks.ts`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/kit/src/types/docks.ts)). `createKitContext` wraps `createHubContext` and only adds Vite config/server slots ([source](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/kit/src/node/context.ts)). + +The workspace catalog and lockfile resolve both `@json-render/core` and `@json-render/vue` to **0.19.0** ([workspace catalog](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/pnpm-workspace.yaml), [lockfile](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/pnpm-lock.yaml)). The core tsdown build explicitly bundles both packages ([`packages/core/tsdown.config.ts`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/tsdown.config.ts)). `packages/core/package.json` still records `0.13.0` under its nonstandard `inlinedDependencies` metadata ([source](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/package.json)); that is inconsistent with the source checkout's actual 0.19.0 resolution and should not be treated as the implementation version without artifact verification. + +### Vue viewer and action bridge + +[`ViewEntry.vue`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/components/views/ViewEntry.vue) lazy-loads the JSON view for `entry.type === 'json-render'`. [`ViewJsonRender.vue`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue) then: + +- reads `entry.ui._stateKey`, obtains the corresponding shared state, takes its current value as an upstream `Spec`, and replaces the shallow spec ref on every update; +- shows local loading/error/empty placeholders; +- mounts `JSONUIProvider` with the registry, a handler object, and `spec.state` as `initialState`, then mounts `Renderer` with the spec and registry; +- watches `_stateKey` and reloads, but does not unsubscribe old shared-state listeners or reset loading/error before a reload; +- does not explicitly remount/reset the provider when a replacement spec contains a different initial state. + +The action bridge is a `markRaw` proxy whose every string property is considered present. Looking up an action lazily creates and caches a function that calls `context.rpc.call(actionName, params)`. It excludes symbols and Promise-like `then`/`catch`/`finally` probes, catches every RPC failure, logs it to the browser console, and resolves without rethrowing. This permits any action string in a spec to address any RPC method available to that client; there is no catalog/action allowlist, declaration check, argument schema, capability check, loading state, result channel, or static-mode policy. + +### Catalog and registry + +[`catalog.ts`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/json-render/catalog.ts) defines a Zod-backed catalog with 14 components and an empty `actions` map. [`registry.ts`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/json-render/registry.ts) is an independently maintained `Record` with the same names: + +| Component | Current behavior to account for | +| --- | --- | +| `Stack` | Flex row/column, numeric pixel gap/padding, alignment/justification; implementation also accepts undocumented `flex`. | +| `Card` | Border/title and component-local collapsible state. | +| `Text` | Heading/body/caption/code tags and inline typography. | +| `Badge` | Five semantic variants; implementation additionally accepts undocumented `title` and `minWidth`. | +| `Button` | Primary/secondary/ghost/danger, optional remote Iconify icon, emits `press` without an event payload. | +| `Icon` | Fetches Iconify SVG at runtime, sanitizes with DOMPurify, and renders `innerHTML`; failed requests can retry. | +| `Divider` | Rule with optional centered label. | +| `TextInput` | `useBoundProp` two-way binding; emits `change` without the new value on every input event. | +| `Switch` | Accessible `role="switch"`, bound boolean state, emits payload-free `change`; thumb color is hardcoded white. | +| `KeyValueTable` | Two-column text/monospace table. | +| `DataTable` | Sticky headers, optional scroll height, index-keyed rows; `rowClick` emits no row or index. | +| `CodeBlock` | Plain escaped `
` with filename/scrolling; declared `language` is unused. |
+| `Progress` | Clamped percentage and animated bar; `max = 0` is not specially handled. |
+| `Tree` | Recursive object/array viewer with local expansion refs and string/number syntax colors. |
+
+Component sources live under [`packages/core/src/client/webcomponents/json-render/components/`](https://github.com/vitejs/devtools/tree/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/json-render/components). They receive the upstream registry contract (`element`, `on`, `bindings`, `loading`) and use `useBoundProp` for the two inputs. Most styling is inline. [`tokens.ts`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/json-render/components/tokens.ts) mixes a few `--jr-*` variables with hardcoded RGB/RGBA and syntax colors. The implementation ignores the provided `loading` prop.
+
+Compatibility should be measured against observable component names, documented props/defaults, event names, binding behavior, and visual roles—not against incidental shortcomings. Items that require an explicit preserve/reconsider decision include payload-free events, undocumented props, ignored `language`/`loading`, runtime Iconify network access, hardcoded colors, provider state reset semantics, listener cleanup, and arbitrary-RPC actions. The upstream docs and examples are in [`docs/kit/json-render.md`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/docs/kit/json-render.md); the only focused coverage is the gallery Storybook file [`JsonRender.stories.ts`](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts), not behavioral tests.
+
+## Repository constraints
+
+The following constraints come from [`AGENTS.md`](../../../AGENTS.md) and existing package boundaries:
+
+1. **Devframe describes one integration and must remain portable.** A JSON-render view that belongs to a devframe must run outside a multi-tool hub through CLI/live/static/embedded contexts. Dock selection, grouping, and other multi-integration UX remain hub concerns.
+2. **The hub is headless.** `@devframes/hub` may own transport/orchestration contracts and dock metadata, but the Vue renderer, component registry, styling, loading surfaces, and default standalone page belong outside the headless hub.
+3. **One definition must survive hosted and standalone deployment.** Hosted defaults use `/__/`; standalone defaults use `/`. SPA assets stay relative and discover connection metadata/dumps from runtime location. No integration may assume the Vite DevTools mount or rewrite HTML per deployment.
+4. **Live and static are distinct capabilities.** Shared-state query snapshots provide static specs; RPC actions do not exist in static dumps. Static action behavior needs an explicit contract and cannot silently inherit the live arbitrary-RPC bridge.
+5. **Shared values are serializable.** Functions are server-local handles and disappear during dock projection. Cross-boundary state needs stable serializable identity, and action/data payloads must respect the selected JSON/structured-clone contract.
+6. **Tool isolation and naming matter.** RPC IDs use `defineRpcFunction` and the repository's namespacing convention; scoped contexts already namespace RPC/shared-state/streaming. The current global renderer counter bypasses that isolation.
+7. **UI extraction must adopt the repository design system.** Vue uses `@antfu/design` components directly; styling uses the common UnoCSS preset, semantic classes/tokens, dark mode, Phosphor icons, and the shared Storybook setup. The Vite implementation's hardcoded palette and bespoke button/input styling are reference behavior, not conforming destination code.
+8. **Packages follow the monorepo contract.** ESM packages under `packages/*` use catalog dependencies, explicit export subpaths, `tsdown`, a package `tsconfig`, `build` and `typecheck` scripts, and committed tsnapi public-API snapshots. Browser/server entry graphs are separated where necessary; see [`packages/devframe/tsdown.config.ts`](../../../packages/devframe/tsdown.config.ts), [`packages/hub/tsdown.config.ts`](../../../packages/hub/tsdown.config.ts), [`pnpm-workspace.yaml`](../../../pnpm-workspace.yaml), and [`tests/exports.test.ts`](../../../tests/exports.test.ts).
+9. **Node diagnostics are structured.** New node warnings/errors use `nostics` and sequential `DF` codes: core uses `DF00xx–DF07xx`, hub JSON/dock concerns fall in the allocated hub ranges (currently docks are `DF81xx`). Browser-only renderer failures may use `console.*`. Existing definitions are in [`packages/devframe/src/node/diagnostics.ts`](../../../packages/devframe/src/node/diagnostics.ts), [`packages/devframe/src/rpc/diagnostics.ts`](../../../packages/devframe/src/rpc/diagnostics.ts), and [`packages/hub/src/node/diagnostics.ts`](../../../packages/hub/src/node/diagnostics.ts).
+
+## Existing test obligations and gaps
+
+- Generic shared-state synchronization has only narrow client coverage ([`packages/devframe/src/client/rpc-shared-state.test.ts`](../../../packages/devframe/src/client/rpc-shared-state.test.ts)); static dump collection and replay have broad generic coverage ([`packages/devframe/src/rpc/dump/__tests__/static.test.ts`](../../../packages/devframe/src/rpc/dump/__tests__/static.test.ts), [`packages/devframe/src/client/static-rpc.test.ts`](../../../packages/devframe/src/client/static-rpc.test.ts)).
+- Hub tests cover dock registration/projection, grouping, activation, shared dock seeding, and client-host reconciliation/scripts ([`packages/hub/src/node/__tests__/host-docks.test.ts`](../../../packages/hub/src/node/__tests__/host-docks.test.ts), [`packages/hub/src/node/__tests__/context.test.ts`](../../../packages/hub/src/node/__tests__/context.test.ts), [`packages/hub/src/client/__tests__/host.test.ts`](../../../packages/hub/src/client/__tests__/host.test.ts)).
+- No local test mentions `createJsonRenderer`, `JsonRenderer`, `_stateKey`, or a `json-render` dock. There is therefore no direct guard for key allocation, update semantics, lossy dock projection, multiple renderers, build ordering, static replay, disposal, or reconnection.
+- The public types are guarded only indirectly by tsnapi snapshots. Upstream has Storybook rendering coverage but no focused action, binding, update, error, or accessibility tests in the JSON-render directory at the pinned commit.
+
+## Consequential compatibility obligations
+
+Without choosing a final public API, the existing behavior establishes these obligations for later design work:
+
+- Preserve a complete serializable spec snapshot and reactive whole-spec replacement; decide whether state-only updates continue as whole snapshots or gain a narrower transport.
+- Preserve the ability for a hub dock to carry only a serializable renderer reference, while correcting the current type/projection mismatch.
+- Keep the renderer/viewer replaceable and outside the headless hub; make the same devframe-owned view reachable in standalone live and static outputs.
+- Account for all 14 component names, documented props/defaults, `press`/`change`/`rowClick`, `$bindState`, and initial state when assessing extraction compatibility.
+- Replace or explicitly constrain the current arbitrary action-string RPC bridge; its permissiveness is observable behavior but conflicts with the plan's declared-action direction and static safety.
+- Treat static rendering as read-only unless a separate static interaction policy is defined; never imply that dumped action RPC will execute.
+- Add behavioral coverage across live shared state, static dumps, hub projection/viewer integration, standalone rendering, component events/bindings, and cleanup. Existing generic tests are useful seams but do not prove JSON-render behavior.
diff --git a/plans/json-render/research/upstream-json-render.md b/plans/json-render/research/upstream-json-render.md
new file mode 100644
index 00000000..ce76642c
--- /dev/null
+++ b/plans/json-render/research/upstream-json-render.md
@@ -0,0 +1,88 @@
+# Upstream json-render contracts
+
+Research date: 2026-07-21.
+
+## Scope and pinned sources
+
+- npm's `latest` tag is **`0.19.0`** for both [`@json-render/core`](https://registry.npmjs.org/@json-render%2Fcore/0.19.0) and [`@json-render/vue`](https://registry.npmjs.org/@json-render%2Fvue/0.19.0). They were published on 2026-05-07. The matching source tag is [`v0.19.0`](https://github.com/vercel-labs/json-render/tree/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7), commit `0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7`.
+- The comparison target is `vitejs/devtools` **`v0.4.2`**, commit [`1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360`](https://github.com/vitejs/devtools/tree/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360), current on the research date. Its json-render dock began at commit [`9840419fb9a9cd82521dd423af05805ae64e63a6`](https://github.com/vitejs/devtools/commit/9840419fb9a9cd82521dd423af05805ae64e63a6) against `0.13.0`; commit [`5851c3fe9415ba0a216e38e60d334dbb0b2d52c9`](https://github.com/vitejs/devtools/commit/5851c3fe9415ba0a216e38e60d334dbb0b2d52c9) raised the workspace catalog to `^0.19.0` without changing the integration.
+
+## Conclusion
+
+`@devframes/json-render` can directly re-export a **curated, named subset** of `@json-render/core@0.19.0`, but those names then become Devframes semver commitments. It should not use `export *`, derive its durable wire contract solely from upstream `Spec`/Vue `schema`, or let an upstream upgrade silently change accepted specs. Pin the exact upstream version, keep a versioned Devframes protocol and validator, and test that protocol against the supported upstream renderer.
+
+The useful core interoperability surface is `defineSchema`, `defineCatalog`, catalog inference types, `Spec`/`UIElement`, state/visibility/prop/action types and resolvers, `StateStore`/`createStateStore`, and optionally the RFC 6902 stream primitives. Vue-specific registries, providers, hooks, and lifecycle remain the responsibility of `@devframes/json-render-ui`. Devframes must own extension composition and the declared-action allowlist because upstream supplies neither as an enforceable runtime boundary.
+
+## Current contracts
+
+### Schema and catalog typing
+
+`defineSchema` describes separate spec and catalog grammars with a small builder (`object`, `record`, `map`, `ref`, `propsOf`, `zod`, and primitives). `defineCatalog` returns component/action names plus `prompt()`, `jsonSchema()`, `validate()`, and `zodSchema()`; inference helpers expose component props and action params. See the pinned [`schema.ts`](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/schema.ts#L9-L105) and [catalog helpers](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/schema.ts#L219-L371).
+
+The typing is useful but is not a complete runtime protocol validator:
+
+- `defineCatalog` does not parse the catalog data at runtime; TypeScript is the catalog-definition guard.
+- With multiple components, `propsOf` deliberately becomes `Record` rather than a discriminator-coupled props union, so `catalog.validate()` checks component names but not per-component props ([implementation](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/schema.ts#L515-L539)).
+- The shipped Vue schema includes `root`, `elements`, `type`, `props`, `children`, and `visible`, but omits top-level `state` and element `on`, `repeat`, and `watch`, although the runtime `Spec`/`UIElement` types support all four ([Vue schema](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/schema.ts#L10-L75), [runtime types](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/types.ts#L53-L78)). The generated Zod objects are ordinary `z.object` schemas, so undeclared fields are not a durable validation contract.
+- Strict JSON Schema cannot represent dynamic-key records and emits them as empty, closed objects; upstream documents this limitation ([`JsonSchemaOptions`](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/schema.ts#L107-L127)).
+
+Devframes should therefore define and validate its complete versioned spec shape itself, while preserving structural compatibility with the selected upstream types.
+
+### Registries and extension composition
+
+Vue's `defineRegistry(catalog, { components, actions })` maps typed render functions into Vue components. Actions are conditionally required when the catalog has action keys, but the catalog argument is unused at runtime and `components` itself is optional. The low-level `Renderer` also accepts any `Record` and an optional fallback ([registry implementation](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/renderer.ts#L739-L888)). Rendering does not Zod-parse resolved props.
+
+There is no upstream catalog/registry extend or merge API, collision policy, extension identity, or version negotiation. Consumers compose object maps before calling `defineCatalog`/`defineRegistry`. `0.19.0` adds composable custom prop directives, but this is a different extension mechanism: `defineDirective` reserves built-in `$` keys, nested directives can resolve one another, and `createDirectiveRegistry` uses last-write-wins for duplicate names ([directives source](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/directives.ts#L24-L123)). Devframes must own explicit catalog-extension composition, duplicate handling, and protocol compatibility.
+
+### State, bindings, and expressions
+
+The state model is `Record` addressed by JSON Pointer. `StateStore` offers `get`, immutable-reference-sensitive `set`, batched `update`, snapshots, and subscription; an external store is supported ([contract](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/types.ts#L182-L215)). Prop expressions include `$state`, repeat-scoped `$item`/`$index`, two-way `$bindState`/`$bindItem`, `$cond`, `$computed`, `$template`, and custom directives; binding resolution separately returns writable absolute paths ([expression contract](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/props.ts#L10-L67), [resolution](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/props.ts#L198-L366)). Visibility supports state/item/index truthiness, comparisons, `not`, implicit/explicit AND, and OR ([visibility source](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/visibility.ts#L240-L348)).
+
+These are renderer-evaluated capabilities, not inert data. Devframes should either adopt each expression in the protocol and test every official renderer, or reject it. `$computed` and custom directives execute host-registered code and should be explicit extensions, not portable base-catalog assumptions.
+
+### Actions and validation
+
+An element event maps to one or more `ActionBinding`s. A binding names an action and can carry dynamic params, confirmation, success/error behavior, and `preventDefault`; handlers may be sync or async ([action contract](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/actions.ts#L31-L131)). Vue resolves bindings sequentially, tracks loading by action name, supports confirmation and chaining, and handles `setState`, `pushState`, `removeState`, and `validateForm` internally ([Vue action provider](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/composables/actions.ts#L145-L337)).
+
+Catalog action `params` schemas are prompt/type metadata: Vue does not parse params against them before dispatch, and unknown runtime actions merely warn. Likewise, unknown validation function names warn and pass as valid ([validation behavior](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/validation.ts#L267-L305)). Built-in field checks and custom validation functions are available, with Vue field registration and `validateForm` aggregation ([Vue validation provider](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/composables/validation.ts#L102-L201)).
+
+Consequently, Devframes' declared-action contract must validate the action name and params before crossing RPC. Built-ins need an explicit reserved list and semantics. Upstream registry typing alone is not an authorization or validation boundary.
+
+### Streaming
+
+Core's SpecStream is newline-delimited RFC 6902 patches. It supplies line parsing, mutable patch application, incremental compilation, mixed prose/spec parsing, AI SDK-compatible `TransformStream` classification, `data-spec` payloads, nested-to-flat conversion, and `pipeJsonRender` without depending on the AI SDK ([stream compiler](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/types.ts#L540-L879), [AI stream contract](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/types.ts#L987-L1305)). Vue adds browser-fetch `useUIStream` and `useChatUI`, aborting requests on unmount ([hooks](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/hooks.ts#L213-L395)).
+
+The stream surface should be optional in the first Devframes contract. It is generation transport, not required for rendering a complete remote spec. If adopted, Devframes should reuse core's patch semantics rather than Vue's separate hook implementation: Vue's local `useUIStream` patcher treats RFC 6902 `test` as a no-op, whereas core throws on a failed test ([Vue patcher](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/hooks.ts#L170-L211), [core patcher](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/src/types.ts#L571-L617)). Validate patches and resource limits before applying untrusted streams.
+
+### Vue renderer lifecycle
+
+`Renderer` returns nothing for a null/missing root, recursively resolves visibility/props/bindings, repeats child templates over state arrays, uses fallback-or-null for unknown components, warns for missing children after loading, and isolates component render errors by rendering that element as null ([renderer](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/renderer.ts#L365-L584)). `JSONUIProvider` nests state, visibility, validation, action, computed-function, and directive providers and adds the default confirmation UI ([provider tree](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/renderer.ts#L607-L737)).
+
+`StateProvider` chooses controlled versus internal state once at setup, subscribes until unmount, and in uncontrolled mode watches later `initialState` changes by flattening them to pointer updates. A replaced `store` prop does not switch stores, and `Renderer` does not automatically seed state from `spec.state`; the caller must pass it to the provider ([state lifecycle](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/vue/src/composables/state.ts#L59-L180)). The official Devframes shell should make remount/reset/preserve-state behavior explicit when view identity or protocol version changes.
+
+## `vitejs/devtools` comparison
+
+The current workspace and lock resolve `@json-render/core` and `@json-render/vue` to `0.19.0` ([catalog](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/pnpm-workspace.yaml#L122-L123), [lock](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/pnpm-lock.yaml#L205-L210)). Its build explicitly bundles both packages ([tsdown config](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/tsdown.config.ts#L14-L44)). However, `packages/core/package.json` still records both inlined dependency versions as `0.13.0` ([stale metadata](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/package.json#L114-L129)). Thus the executable integration is `0.19.0`; `0.13.0` describes its origin and stale attribution metadata, not the resolved bundle.
+
+Its behavior remains the original `0.13.0` integration:
+
+- A custom schema/catalog lists the built-in components but omits state, visibility, repeat, watch, and event fields and declares no actions ([catalog](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/json-render/catalog.ts#L8-L153)). The catalog is exported but never used to validate specs.
+- A raw Vue component record is passed directly to `Renderer`, bypassing `defineRegistry` and catalog-derived component checks ([registry](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/json-render/registry.ts#L17-L32)). It uses upstream `useBoundProp` for inputs and switches.
+- `ViewJsonRender` subscribes to a shared complete spec, feeds `spec.state` into `JSONUIProvider`, and replaces the rendered spec on updates. Its `Proxy` claims every action exists and calls an RPC whose name is the unvalidated action string ([view](https://github.com/vitejs/devtools/blob/1b13d847a3fbbdc6ec1b2940f36fd6d3fdf60360/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue#L18-L104)). This arbitrary action-to-RPC bridge is precisely the behavior Devframes should replace with per-view declarations and param validation.
+- It does not use upstream streaming, catalog validation, field validation, computed functions, directives, or extension composition. The `0.13.0` to `0.19.0` changes relevant to this integration are additive: edit/diff helpers, devtools hooks, improved Zod prompt formatting, and `0.19.0` directives ([upstream changelog](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/CHANGELOG.md#L3-L34)).
+
+## Package shape, stability, and license
+
+`@json-render/core@0.19.0` exports `.` and `./store-utils` in ESM and CommonJS, depends on `zod ^4.3.6`, and also declares `zod ^4.0.0` as a peer ([package](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/package.json)). `@json-render/vue@0.19.0` exports `.` and the server-safe `./schema`, depends exactly on core `0.19.0`, and peers on `vue ^3.5.0` and `zod ^4.0.0` ([npm manifest](https://registry.npmjs.org/@json-render%2Fvue/0.19.0)). Catalog types expose Zod types publicly, so Devframes should align one Zod 4 instance and keep Vue/core versions paired.
+
+The project is pre-1.0 and young: core published `0.0.1` on 2026-01-14 and reached `0.19.0` by 2026-05-07; Vue moved from `0.9.1` on 2026-02-25 to `0.19.0` in ten weeks ([core registry history](https://registry.npmjs.org/@json-render%2Fcore), [Vue registry history](https://registry.npmjs.org/@json-render%2Fvue)). Documented breaking changes have already renamed the expression language, repeat/action fields, provider props, generation modes, and removed APIs ([core changelog](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/packages/core/CHANGELOG.md#L300-L363)). Exact pinning and an explicit upstream-upgrade review are warranted.
+
+Both packages and the repository are **Apache-2.0** ([license](https://github.com/vercel-labs/json-render/blob/0bbe6ed6394b23b5aee25320d03c9b7ac717e5b7/LICENSE)); the pinned tree has no `NOTICE` file. Direct dependency/re-export is compatible with Devframes' licensing, while any redistributed or bundled upstream object code must include the Apache 2.0 license, retain applicable notices, and mark modified upstream files. The license also includes a patent grant and patent-litigation termination clause. Package naming and documentation must not imply Vercel endorsement or trademark rights.
+
+## Recommended boundary
+
+1. Make the Devframes protocol schema, catalog version, extension IDs, action declarations, and validation/error behavior authoritative.
+2. Re-export only reviewed core names with explicit export lists; treat additions/removals as Devframes API changes and pin `0.19.0` until compatibility is retested.
+3. Keep upstream prompt/generation/edit/devtools hooks and Vue streaming hooks outside the portable base. Add opt-in subpaths later if required.
+4. Validate complete specs, component props, action names, and action params at trust boundaries. Do not copy Vite DevTools' catch-all RPC proxy.
+5. Let `@devframes/json-render-ui` adapt the Devframes protocol to `@json-render/vue`, own provider lifecycle and registry composition, and expose third-party registry replacement without making Vue part of the protocol package.

From 316505bf4a50556c6195699cdd1613c0bde31d57 Mon Sep 17 00:00:00 2001
From: "Anthony Fu (via agent)" 
Date: Tue, 21 Jul 2026 10:11:06 +0000
Subject: [PATCH 2/3] docs: collapse json-render wayfinder into a single
 concrete plan

---
 .../01-research-upstream-json-render.md       |  15 --
 .../02-research-existing-integration-seams.md |  15 --
 .../issues/03-define-package-contracts.md     |  13 --
 .../issues/04-define-protocol-semantics.md    |  13 --
 .../05-define-devframe-runtime-integration.md |  13 --
 .../issues/06-define-hub-projection.md        |  13 --
 .../issues/07-define-official-vue-ui.md       |  13 --
 .../issues/08-prototype-consumer-api.md       |  13 --
 .../issues/09-assemble-implementation-spec.md |  13 --
 plans/json-render/map.md                      |  33 ----
 plans/json-render/plan.md                     | 176 ++++++++++++++++++
 11 files changed, 176 insertions(+), 154 deletions(-)
 delete mode 100644 plans/json-render/issues/01-research-upstream-json-render.md
 delete mode 100644 plans/json-render/issues/02-research-existing-integration-seams.md
 delete mode 100644 plans/json-render/issues/03-define-package-contracts.md
 delete mode 100644 plans/json-render/issues/04-define-protocol-semantics.md
 delete mode 100644 plans/json-render/issues/05-define-devframe-runtime-integration.md
 delete mode 100644 plans/json-render/issues/06-define-hub-projection.md
 delete mode 100644 plans/json-render/issues/07-define-official-vue-ui.md
 delete mode 100644 plans/json-render/issues/08-prototype-consumer-api.md
 delete mode 100644 plans/json-render/issues/09-assemble-implementation-spec.md
 delete mode 100644 plans/json-render/map.md
 create mode 100644 plans/json-render/plan.md

diff --git a/plans/json-render/issues/01-research-upstream-json-render.md b/plans/json-render/issues/01-research-upstream-json-render.md
deleted file mode 100644
index a2a74adc..00000000
--- a/plans/json-render/issues/01-research-upstream-json-render.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Research upstream json-render contracts
-
-Type: research
-Status: resolved
-Blocked by: none
-
-## Question
-
-Which public contracts and runtime semantics in the current `@json-render/core` and `@json-render/vue` releases must the Devframes packages adopt, re-export, configure, or deliberately constrain? Compare the current release with the version embedded by `vitejs/devtools`, covering schema/catalog typing, registries, state and binding expressions, actions and validation, streaming, extension composition, renderer lifecycle, package stability, and licensing.
-
-## Answer
-
-See [Upstream json-render contracts](../research/upstream-json-render.md).
-
-The current upstream pair is `@json-render/core@0.19.0` and `@json-render/vue@0.19.0`. Its catalog and registry types aid authoring but do not fully validate component props, authorize actions, compose extensions, or guarantee lifecycle behavior. A direct Devframes re-export therefore needs reviewed named exports, exact upstream pinning, a separately versioned protocol validator, and compatibility tests. The report also records the Apache-2.0 obligations and recommends keeping streaming optional until the protocol decision explicitly adopts it.
diff --git a/plans/json-render/issues/02-research-existing-integration-seams.md b/plans/json-render/issues/02-research-existing-integration-seams.md
deleted file mode 100644
index 1332def3..00000000
--- a/plans/json-render/issues/02-research-existing-integration-seams.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Inventory existing JSON-render and runtime seams
-
-Type: research
-Status: resolved
-Blocked by: none
-
-## Question
-
-What behavior currently exists in this repository and `vitejs/devtools`, and which devframe and hub seams can host it without violating their boundaries? Trace the current hub types, `createJsonRenderer`, shared-state transport, dock projection, Vite viewer, standalone adapters, static dumps, client host, package build conventions, and tests. Record concrete compatibility obligations and architectural constraints rather than proposing the final API.
-
-## Answer
-
-See [Existing JSON-render integration seams](../research/existing-integration-seams.md).
-
-The current repository owns permissive hub types, a hub-only shared-state factory, and a JSON-render dock discriminator; `vitejs/devtools` owns the only renderer and catalog. Wire projection accidentally strips renderer methods and leaves `_stateKey`, standalone adapters have no renderer assets or factory, and static dumps can replay specs but not RPC actions. The hub's existing headless client boundary is suitable for renderer substitution, while stable scoped identities, explicit serialization, cleanup, standalone assets, declared-action isolation, and dedicated tests are missing.
diff --git a/plans/json-render/issues/03-define-package-contracts.md b/plans/json-render/issues/03-define-package-contracts.md
deleted file mode 100644
index eb6da10e..00000000
--- a/plans/json-render/issues/03-define-package-contracts.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Define package ownership and public exports
-
-Type: grilling
-Status: open
-Blocked by: 01, 02
-
-## Question
-
-What does each package own and export, and what dependency direction keeps the protocol stable, devframe-native, and renderer-swappable? Resolve the direct `@json-render/core` re-export surface, schema and catalog exports, renderer handles, Vue registry and shell exports, subpath exports, peer dependencies, type names, and the boundary with `devframe` and `@devframes/hub`.
-
-## Answer
-
-Pending.
diff --git a/plans/json-render/issues/04-define-protocol-semantics.md b/plans/json-render/issues/04-define-protocol-semantics.md
deleted file mode 100644
index 0eb1fd4d..00000000
--- a/plans/json-render/issues/04-define-protocol-semantics.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Define catalog, state, and action semantics
-
-Type: grilling
-Status: open
-Blocked by: 01, 03
-
-## Question
-
-What makes a Devframes JSON-render spec valid and portable? Define the versioned base catalog, extension catalogs and compatibility metadata, declared and validated actions, state ownership and binding updates, replacement versus incremental updates, whether streaming/generative-UI primitives enter the initial protocol, serialization limits, validation timing, failure behavior, and how direct upstream re-exports coexist with a stable Devframes protocol.
-
-## Answer
-
-Pending.
diff --git a/plans/json-render/issues/05-define-devframe-runtime-integration.md b/plans/json-render/issues/05-define-devframe-runtime-integration.md
deleted file mode 100644
index e5994ea8..00000000
--- a/plans/json-render/issues/05-define-devframe-runtime-integration.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Define devframe runtime integration
-
-Type: grilling
-Status: open
-Blocked by: 02, 03, 04
-
-## Question
-
-How does an author declare and update one or more JSON-render views on a devframe, and how does the same definition run through CLI, dev, SPA, build, Vite, and embedded adapters? Resolve view identity and lifecycle, scoped shared state and RPC actions, static snapshot behavior, default standalone UI assets, ownership of `createJsonRenderer`, disposal, reconnects, and whether the integration requires a general devframe view primitive.
-
-## Answer
-
-Pending.
diff --git a/plans/json-render/issues/06-define-hub-projection.md b/plans/json-render/issues/06-define-hub-projection.md
deleted file mode 100644
index 57ad2f08..00000000
--- a/plans/json-render/issues/06-define-hub-projection.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Define hub projection and renderer substitution
-
-Type: grilling
-Status: open
-Blocked by: 03, 04, 05
-
-## Question
-
-How does a hub discover and project a devframe JSON-render view into a dock while remaining headless and UI-framework neutral? Define dock metadata, client-host responsibilities, renderer selection and injection, use of the official Vue UI versus a third-party implementation, catalog compatibility checks, loading and error states, disposal, and migration of the current hub-owned JSON-render types and factory.
-
-## Answer
-
-Pending.
diff --git a/plans/json-render/issues/07-define-official-vue-ui.md b/plans/json-render/issues/07-define-official-vue-ui.md
deleted file mode 100644
index 6f3b1c1d..00000000
--- a/plans/json-render/issues/07-define-official-vue-ui.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Define the official Vue and Antfu Design UI
-
-Type: grilling
-Status: open
-Blocked by: 04
-
-## Question
-
-What exactly does `@devframes/json-render-ui` ship as the opinionated Vue implementation? Resolve its base component registry, fidelity and intentional changes from the Vite DevTools catalog, `@antfu/design` component mapping, semantic tokens and UnoCSS integration, icons, accessibility, loading and error surfaces, registry extension, renderer shell APIs, static-app assets, Storybook coverage, and theming responsibilities.
-
-## Answer
-
-Pending.
diff --git a/plans/json-render/issues/08-prototype-consumer-api.md b/plans/json-render/issues/08-prototype-consumer-api.md
deleted file mode 100644
index 1cfdd5f7..00000000
--- a/plans/json-render/issues/08-prototype-consumer-api.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Prototype the end-to-end consumer API
-
-Type: prototype
-Status: open
-Blocked by: 05, 06, 07
-
-## Question
-
-Does one small, disposable API prototype make the intended seams natural for all key consumers? Demonstrate one view authored once, rendered by the official Vue registry standalone and in a hub dock, replaced by a third-party registry, updated through declared live actions and state, and emitted as a static snapshot. Use the prototype to resolve awkward ownership, naming, lifecycle, and configuration before the specification is assembled.
-
-## Answer
-
-Pending.
diff --git a/plans/json-render/issues/09-assemble-implementation-spec.md b/plans/json-render/issues/09-assemble-implementation-spec.md
deleted file mode 100644
index 95140e20..00000000
--- a/plans/json-render/issues/09-assemble-implementation-spec.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Assemble the implementation-ready specification
-
-Type: task
-Status: open
-Blocked by: 01, 02, 03, 04, 05, 06, 07, 08
-
-## Question
-
-Consolidate the resolved decisions and prototype into the destination specification without reopening them. Include package manifests and exports, public TypeScript API sketches, runtime and data flows, component catalog, extension and substitution model, devframe and hub changes, migration of existing repository APIs, diagnostics, documentation, test matrix, acceptance criteria, and an ordered implementation sequence suitable for follow-up tickets.
-
-## Answer
-
-Pending.
diff --git a/plans/json-render/map.md b/plans/json-render/map.md
deleted file mode 100644
index 818b218b..00000000
--- a/plans/json-render/map.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# JSON Render Packages Wayfinder
-
-## Destination
-
-Produce an implementation-ready specification for `@devframes/json-render` and `@devframes/json-render-ui`: their package boundaries, public APIs, protocol semantics, official Vue renderer, devframe runtime support, hub integration, migration inside this repository, acceptance criteria, and implementation sequence.
-
-## Notes
-
-- This map plans the work; implementing or releasing the packages is outside its destination.
-- `@devframes/json-render` is a stable, devframe-native protocol package. It directly re-exports the relevant `@json-render/core` API and owns the Devframes catalog, declared-action contract, runtime handles, and integration helpers.
-- `@devframes/json-render-ui` is the official Vue implementation. Its registry implements the catalog with `@antfu/design`, while third-party registries can replace it.
-- The catalog has a versioned portable base and explicit extensions. A registry is a framework-specific implementation of a catalog.
-- Views work in standalone and hub deployments across live and static devframe outputs. Live outputs can use RPC actions; static behavior must be specified explicitly.
-- Specs invoke only actions declared for that view. An arbitrary string-to-RPC bridge is not part of the target contract.
-- The final specification covers this repository. Adoption and code removal in `vitejs/devtools` is a separate effort.
-- Relevant skills: `research`, `grilling`, `domain-modeling`, `prototype`, `antfu`, `antfu-design`, `vue-best-practices`, `unocss`, `vite`, `tsdown`, and `vitest`.
-- Local tracker operations: tickets are files under `issues/`; `Status: open` is unclaimed, `Status: claimed` is in progress, and `Status: resolved` is closed. A ticket is on the frontier when every number in `Blocked by` is resolved.
-
-## Decisions so far
-
-- [Research upstream json-render contracts](issues/01-research-upstream-json-render.md) — Upstream `0.19.0` is structurally reusable but pre-1.0 typing is not a validation or authorization boundary; direct re-exports require pinning and a Devframes-owned protocol guard.
-- [Inventory existing JSON-render and runtime seams](issues/02-research-existing-integration-seams.md) — The current hub transports whole specs through shared state and an accidental `_stateKey` projection; portable standalone/static rendering, stable identity, action isolation, cleanup, and tests remain to be designed.
-
-## Not yet specified
-
-- The implementation slices and test matrix. These become precise after the public API prototype establishes the package seams.
-
-## Out of scope
-
-- Implementing, publishing, or releasing the packages.
-- Changing `vitejs/devtools` to consume the packages or deleting its current implementation.
-- Official React, Solid, Svelte, or vanilla registries; third-party renderer compatibility is specified through the core contract.
-- Agent-driven UI generation beyond any protocol capability required to keep the base catalog compatible with json-render.
diff --git a/plans/json-render/plan.md b/plans/json-render/plan.md
new file mode 100644
index 00000000..aa7222fa
--- /dev/null
+++ b/plans/json-render/plan.md
@@ -0,0 +1,176 @@
+# JSON Render Packages — Implementation Plan
+
+A single, concrete plan for `@devframes/json-render` and `@devframes/json-render-ui`. It resolves every open decision from the earlier wayfinder map into firm choices and an ordered implementation sequence, grounded in two primary-source studies kept alongside this file:
+
+- [Upstream json-render contracts](research/upstream-json-render.md) — `@json-render/core@0.19.0` / `@json-render/vue@0.19.0`.
+- [Existing JSON-render integration seams](research/existing-integration-seams.md) — this repo and `vitejs/devtools@v0.4.2`.
+
+Scope is this repository. Implementing, publishing, or releasing the packages, and adopting them in `vitejs/devtools`, are out of scope. Where a choice is genuinely contestable it is marked **Decision** with the reasoning; treat those as the default to build against, not as reopened questions.
+
+## Destination
+
+Two packages plus the devframe/hub changes that host them:
+
+- `@devframes/json-render` — the devframe-native, framework-neutral protocol package. Owns the versioned spec schema, the versioned base catalog and extension model, the declared-action contract and validator, the runtime view factory (`createJsonRenderView`), and a curated re-export of `@json-render/core`. No Vue, no DOM, no presentation.
+- `@devframes/json-render-ui` — the official Vue renderer. Implements the base catalog with `@antfu/design`, ships the renderer shell, the default standalone SPA, and Storybook. Third-party registries can replace it.
+
+A single view definition authored once renders standalone (CLI/dev/build/SPA/embedded) and inside a hub dock, live or static, with the official Vue UI or a substituted registry.
+
+## 1. Package ownership and exports (resolves decision 03)
+
+### `@devframes/json-render`
+
+Location `packages/json-render/`. ESM, `tsdown`, own `tsconfig.json` (extends `tsconfig.base.json`), `build` + `typecheck` scripts, committed tsnapi snapshot. Two entry graphs kept separate so the node factory never pulls browser code:
+
+| Subpath | Contents |
+| --- | --- |
+| `.` (isomorphic) | Protocol schema + types, base catalog, extension composition, action-declaration builder, validator, catalog-compatibility helper. Pure data + Zod; safe in node and browser. |
+| `./node` | `createJsonRenderView` and devframe integration helpers (touch RPC/shared-state/scope). Node only. |
+| `./core` | Curated named re-exports of `@json-render/core@0.19.0` (see below). |
+
+**Curated core re-export (`./core`).** Explicit named list, never `export *`. Each name is a Devframes semver commitment; adding or removing one is a Devframes API change. Initial set, per the upstream study: `defineSchema`, `defineCatalog`, catalog inference types, `Spec`, `UIElement`, state/visibility/prop/action types and resolvers, `StateStore`, `createStateStore`. Excluded from the base: prompt/generation/edit/devtools hooks and all streaming primitives (may return later as an opt-in `./core/stream` subpath). `@json-render/core` is pinned to **exact `0.19.0`**; bumping it requires re-running the compatibility tests.
+
+**Dependencies.** `@json-render/core` exact-pinned (catalog `inlined`), `zod ^4` aligned to core's instance (catalog `types`/`inlined`). `devframe` is a **peer** (the node helpers augment a devframe context; they never fork it). No dependency on `@devframes/hub`.
+
+**Type names.** `DevframeJsonRenderSpec`, `DevframeJsonRenderCatalog`, `JsonRenderView`, `JsonRenderViewRef`, `JsonRenderActionDeclaration`, `JsonRenderExtension`. These replace the hub's hand-written `JsonRenderSpec` / `JsonRenderer`.
+
+### `@devframes/json-render-ui`
+
+Location `packages/json-render-ui/`. Same monorepo contract plus a Storybook. Subpaths:
+
+| Subpath | Contents |
+| --- | --- |
+| `.` | `createRenderer()` shell, base Vue registry, `defineRegistry`/`extendRegistry`, provider lifecycle wiring, loading/error surfaces. |
+| `./components` | Individual ported components for direct import / third-party reuse. |
+| `./spa` | Prebuilt standalone SPA entry assets (relative base) consumed by devframe adapters. |
+
+**Dependencies.** `@devframes/json-render` (peer + workspace), `@json-render/vue` exact-pinned `0.19.0` (paired with core), `vue ^3.5` (peer), `@antfu/design` (dev, build-time). Never a dependency of the protocol package — Vue stays out of the protocol.
+
+### Dependency direction
+
+```
+@devframes/json-render-ui ──▶ @devframes/json-render ──▶ devframe (peer)
+                                        │                 @json-render/core (pinned)
+@devframes/hub ─────────────────────────┘  (dock discriminator references JsonRenderViewRef)
+```
+
+The hub depends on `@devframes/json-render` only for the serializable `JsonRenderViewRef` used in its dock discriminator. It never imports the Vue UI.
+
+## 2. Protocol: catalog, state, actions (resolves decision 04)
+
+**Devframes owns the wire contract.** A `DevframeJsonRenderSpec` is validated against a Devframes-authored Zod schema, structurally compatible with core `Spec`/`UIElement` but not derived from upstream typing (the study shows upstream typing checks names, not per-component props or actions). Every spec carries `protocolVersion: 1` and `catalog: { id, version, extensions: [{ id, version }] }`.
+
+**Base catalog v1** = the fourteen components inventoried in the seams study (`Stack`, `Card`, `Text`, `Badge`, `Button`, `Icon`, `Divider`, `TextInput`, `Switch`, `KeyValueTable`, `DataTable`, `CodeBlock`, `Progress`, `Tree`), with props/defaults normalized: documented props only, typed event payloads (`Button.press`, `TextInput.change`, `Switch.change`, `DataTable.rowClick` carry their value/row/index), and `language`/`loading` honored. Undocumented upstream props (`Stack.flex`, `Badge.title`/`minWidth`) are either promoted to documented props or dropped — each decided in the component port, not silently accepted. Catalog is versioned (`catalogVersion` independent of `protocolVersion`).
+
+**Extensions.** An extension is `{ id, version, components, actions }`. Composition is explicit and collision-on-duplicate is an **error** (no upstream last-write-wins). Base is the portable floor; `$computed` and custom directives, which execute host code, are extension-only capabilities, never base assumptions.
+
+**State.** `Record` addressed by JSON Pointer, JSON-serializable. Spec structure is replaced whole on update; **state updates travel as JSON-Pointer patches** over shared state (enable patches for this server-created state — they are currently disabled, per the seams study). This keeps state syncs narrow without a whole-spec broadcast.
+
+**Actions — declared allowlist, no catch-all bridge.** This is the central correction to the Vite implementation's arbitrary action-string→RPC proxy. Each view declares its actions up front:
+
+```ts
+declareAction('reload', { params: z.object({ id: z.string() }) })
+```
+
+- Client dispatch: the action name must be in the view's allowlist and params must parse against the declared schema **before** any RPC crosses the boundary. Unknown names refuse to dispatch (no probing a Proxy).
+- Server: the RPC handler re-validates name + params at the trust boundary.
+- Reserved client-local built-ins (no RPC): `setState`, `pushState`, `removeState`, `validateForm`, matching upstream semantics.
+
+**Serialization.** Strict JSON only for specs and state; the shared-state getter is marked `jsonSerializable: true` so the obligation is enforced, not assumed.
+
+**Validation timing & failure.** Validate spec on the server at declaration and on every update; reject invalid specs with a `DF` diagnostic. Validate action name + params on the client before dispatch and again on the server. Browser-side render failures follow upstream's per-element isolation (render that element as null) and use `console.*`; node-side failures use `nostics`.
+
+**Streaming / generative UI.** Excluded from v1. A complete remote spec renders without it. If adopted later, reuse core's patch semantics (core throws on failed RFC-6902 `test`; the Vue hook treats it as a no-op) behind an opt-in subpath, with patch validation and resource limits.
+
+## 3. Devframe runtime integration (resolves decision 05)
+
+The JSON-render view is a **devframe-level** integration, not hub-only — it must run standalone. `createJsonRenderView` lives in `@devframes/json-render/node` and augments a base `DevframeNodeContext` through its existing scope/RPC/shared-state/views surfaces. The hub's `createJsonRenderer` is removed and its callers migrated.
+
+```ts
+const view = createJsonRenderView(ctx, {
+  id: 'metrics',            // author-provided, stable
+  spec,
+  actions: { reload, clear },
+})
+view.update(spec)
+view.patchState([{ op: 'replace', path: '/count', value: 3 }])
+view.dispose()
+```
+
+- **Identity.** Scoped stable IDs `devframe:json-render::`, replacing the global allocation-order counter. Stable across reconnects and rebuilds.
+- **Lifecycle.** `dispose()` unregisters the shared state and its listeners; no leak for the context lifetime.
+- **Scoped actions.** Registered as scoped RPC via `defineRpcFunction` under the namespacing convention, validated as in §2.
+- **Static.** Build dumps the spec + state as a shared-state query snapshot (read-only render). Actions are **not** dumped; a declared static policy renders action-bound controls in an explicit "unavailable in static output" state rather than silently inheriting live behavior. Local input bindings still work in the renderer.
+- **Standalone UI assets.** When a definition declares json-render views and the author ships no SPA, the `dev`/`build`/`spa` adapters serve `@devframes/json-render-ui/spa` (relative base, runtime connection discovery preserved). Author SPAs still win.
+- **View primitive.** Reuse the existing devframe `views` surface; the json-render view is a typed specialization, not a new primitive.
+
+## 4. Hub projection and renderer substitution (resolves decision 06)
+
+- **Serializable dock ref.** The dock discriminator becomes `{ type: 'json-render', view: JsonRenderViewRef }` where `JsonRenderViewRef = { stateKey, protocolVersion, catalog }` — no functions. This fixes the accidental `_stateKey` projection and the current type/projection mismatch where the projected `JsonRenderer` overstates what crosses the wire.
+- **Headless host.** `createDevframeClientHost` gains a `json-render` branch that resolves a renderer implementation from a host-registered registry (default `@devframes/json-render-ui`, overridable by the UI kit), checks catalog compatibility (`catalog.id`/`version`/extensions) before mounting, drives loading/error states, and disposes on dock deactivation (unsubscribing shared-state listeners the Vite viewer currently leaks). The hub package itself acquires no Vue.
+- **Migration.** `packages/hub/src/types/json-render.ts`, `defineJsonRenderSpec`, and `createJsonRenderer` move to `@devframes/json-render`; the hub re-exports `JsonRenderViewRef` for its dock type only. tsnapi snapshots for `@devframes/hub` update accordingly.
+
+## 5. Official Vue + Antfu Design UI (resolves decision 07)
+
+`@devframes/json-render-ui` ships:
+
+- **Base registry** implementing catalog v1's fourteen components, each ported to `@antfu/design` semantic tokens (`bg-base`/`color-muted`/`border-base`…), the shared UnoCSS preset stack, Phosphor `i-ph:*` icons, and dark mode via `.dark`. Reference behavior is the Vite catalog; conforming code replaces its hardcoded RGB palette and bespoke button/input styling.
+- **Intentional changes from the Vite catalog:** typed event payloads; honor `language` (syntax highlighting) and `loading`; **Decision** — replace runtime Iconify network fetch + DOMPurify `innerHTML` with build-time Phosphor icons mapped by name (removes per-render network access and an HTML-injection surface); provider state reset made explicit on view-identity/protocol change (remount/reset vs preserve).
+- **Renderer shell** wiring `JSONUIProvider` + `Renderer` with the Devframes action bridge (declared allowlist, not the catch-all proxy), seeding `spec.state`, and owning remount/reset semantics.
+- **Registry extension** via `defineRegistry`/`extendRegistry` for catalog extensions; third-party registries replace the whole registry.
+- **Standalone SPA** entry (relative assets, OS-preference dark flip) under `./spa`.
+- **Storybook** following the repo's one setup (co-located stories, `viteFinal` + `unocss/vite`, `.dark` toggle, `bg-base color-base` decorator) covering every component, loading/error, and a full sample view.
+
+## 6. End-to-end prototype (resolves decision 08)
+
+Before the spec is frozen, build one disposable prototype (scratch under `examples/`, deleted after) exercising all seams: one view authored once; rendered standalone by the official Vue registry; mounted in a hub dock; re-rendered by a swapped third-party registry; updated through a declared live action and a state patch; emitted as a static snapshot. Its only output is confirmation (or correction) of ownership, naming, lifecycle, and configuration ergonomics feeding §7.
+
+## 7. Implementation sequence
+
+Each step is an independently reviewable slice; ⟶ marks the dependency.
+
+1. **Scaffold `@devframes/json-render`** — manifest, `tsdown.config.ts` (isomorphic/node/core graphs), `tsconfig.json`, `typecheck` script, empty tsnapi snapshot, catalog deps, exact core pin.
+2. **Protocol core** ⟶1 — spec Zod schema, base catalog v1, extension composition (collision = error), catalog-compatibility helper, validator, `DF` diagnostics. Unit tests.
+3. **Declared-action contract** ⟶2 — action builder, param validation, reserved built-ins, client + server validators.
+4. **Runtime factory** ⟶3 — `createJsonRenderView` (scoped IDs, shared state, JSON-Pointer state patches enabled, scoped action RPC, `dispose`), `jsonSerializable` getter.
+5. **Hub migration** ⟶4 — move types/factory out of hub, `JsonRenderViewRef` dock discriminator, correct projection, update hub tsnapi.
+6. **Scaffold `@devframes/json-render-ui` + base registry** ⟶2 — package, fourteen ported components, tokens, Storybook.
+7. **Renderer shell + action bridge** ⟶3,6 — provider/renderer wiring, declared-action bridge, loading/error, reset semantics.
+8. **Client-host projection** ⟶5,7 — `json-render` branch, renderer registry/injection, compatibility check, lifecycle/disposal.
+9. **Standalone adapters + SPA** ⟶4,7 — serve default SPA from `./spa`; static snapshot + static-action policy.
+10. **Prototype** ⟶8,9 — validate ergonomics; feed corrections back.
+11. **Test matrix** ⟶ across — see §8.
+12. **Docs + error pages** ⟶ across — package docs, `docs/errors/DFxxxx.md` for each new code.
+
+## 8. Test matrix (resolves the map's deferred slices)
+
+Behavioral coverage, since the seams study shows no current test touches `createJsonRenderer`, `_stateKey`, or a json-render dock:
+
+- Protocol: valid/invalid spec, per-component prop validation, extension collision, catalog compatibility.
+- Actions: allowlist enforcement, param schema pass/fail, reserved built-ins, server re-validation, rejection of undeclared names.
+- Runtime: scoped ID stability, `update`/`patchState`, disposal + listener cleanup, multiple concurrent views, reconnect.
+- Transport: whole-spec replace vs state patch, `jsonSerializable` enforcement, shared-state broadcast.
+- Hub: serializable dock projection (no functions), client-host renderer selection/injection, compatibility gate, deactivation disposal.
+- Standalone: live dev render; static build ordering (states registered before dump), static replay read-only, static-action policy.
+- Vue UI: each component's props/defaults/events/bindings (`press`/`change`/`rowClick`/`$bindState`), loading/error, reset-on-identity-change; Storybook render.
+- Snapshots: tsnapi for both packages and the updated hub; `tests/exports.test.ts` subpaths.
+
+## 9. Diagnostics
+
+New node diagnostics use `nostics` with sequential `DF` codes. `@devframes/json-render` is core-side protocol/runtime → allocate in the core range (next free after the current highest `DF00xx`); hub projection changes stay in the hub dock range (`DF81xx`). Suggested set: invalid spec, unknown/invalid action, catalog incompatibility, duplicate extension id, view id collision, disposed-view use. Each gets a `docs/errors/DFxxxx.md` page. Browser-only render failures keep `console.*`.
+
+## 10. Acceptance criteria
+
+- One view definition renders standalone (dev/build/spa/embedded) and in a hub dock, live and static, with the official Vue UI and a substituted registry.
+- No arbitrary action-string→RPC path exists anywhere; every action is declared and param-validated on both sides.
+- Dock projection carries only serializable data; no function survives to the wire, and the projected type matches what the client receives.
+- Views have stable scoped IDs and dispose cleanly with no leaked shared state or listeners.
+- The hub and devframe packages acquire no Vue; the Vue UI is fully replaceable.
+- `pnpm lint && pnpm test && pnpm typecheck && pnpm build` pass, with tsnapi snapshots and exports tests updated.
+
+## Out of scope
+
+- Implementing, publishing, or releasing the packages (this plan is the specification).
+- Changing `vitejs/devtools` to consume the packages or deleting its current implementation.
+- Official React/Solid/Svelte/vanilla registries — third-party compatibility is specified through the core contract only.
+- Agent-driven / generative UI beyond keeping the base catalog compatible with json-render.

From 39ca7ab1952515c46f275053f03f7b0d83757411 Mon Sep 17 00:00:00 2001
From: "Anthony Fu (via agent)" 
Date: Tue, 21 Jul 2026 15:39:45 +0000
Subject: [PATCH 3/3] docs: revise json-render plan to opt-in,
 upstream-contract architecture

---
 plans/json-render/plan.md | 229 ++++++++++++++++++--------------------
 1 file changed, 111 insertions(+), 118 deletions(-)

diff --git a/plans/json-render/plan.md b/plans/json-render/plan.md
index aa7222fa..7491123c 100644
--- a/plans/json-render/plan.md
+++ b/plans/json-render/plan.md
@@ -1,176 +1,169 @@
 # JSON Render Packages — Implementation Plan
 
-A single, concrete plan for `@devframes/json-render` and `@devframes/json-render-ui`. It resolves every open decision from the earlier wayfinder map into firm choices and an ordered implementation sequence, grounded in two primary-source studies kept alongside this file:
+A single, concrete plan for `@devframes/json-render` and `@devframes/json-render-ui`, reflecting the decisions settled in a full design-tree interview. It is grounded in two primary-source studies kept alongside this file:
 
 - [Upstream json-render contracts](research/upstream-json-render.md) — `@json-render/core@0.19.0` / `@json-render/vue@0.19.0`.
 - [Existing JSON-render integration seams](research/existing-integration-seams.md) — this repo and `vitejs/devtools@v0.4.2`.
 
-Scope is this repository. Implementing, publishing, or releasing the packages, and adopting them in `vitejs/devtools`, are out of scope. Where a choice is genuinely contestable it is marked **Decision** with the reasoning; treat those as the default to build against, not as reopened questions.
+Scope is this repository. This PR is **documentation only** — the plan and research; no package scaffolding or code. Implementing, publishing, or releasing the packages, and adopting them in `vitejs/devtools`, are follow-up efforts.
 
-## Destination
+This plan intentionally **supersedes** the earlier wayfinder destination on two points: it does **not** adopt a declared-action allowlist (the action bridge is unrestricted, §4), and it **defers** incremental named catalog extensions (v1 replaces the whole registry instead, §7).
 
-Two packages plus the devframe/hub changes that host them:
+## Destination
 
-- `@devframes/json-render` — the devframe-native, framework-neutral protocol package. Owns the versioned spec schema, the versioned base catalog and extension model, the declared-action contract and validator, the runtime view factory (`createJsonRenderView`), and a curated re-export of `@json-render/core`. No Vue, no DOM, no presentation.
-- `@devframes/json-render-ui` — the official Vue renderer. Implements the base catalog with `@antfu/design`, ships the renderer shell, the default standalone SPA, and Storybook. Third-party registries can replace it.
+Two opt-in packages plus the devframe/hub changes that host them, so that JSON-render is a capability a devframe app *adds*, never a cost a plain app pays:
 
-A single view definition authored once renders standalone (CLI/dev/build/SPA/embedded) and inside a hub dock, live or static, with the official Vue UI or a substituted registry.
+- `@devframes/json-render` — the opt-in, framework-neutral protocol layer. Augments a devframe context with a view factory; owns the spec/catalog types, the base catalog and its Devframes-authored prop schemas, the serializable view reference, and (when a hub is present) contributes the `json-render` dock type. Uses upstream `@json-render/core` as its wire contract. No Vue, no DOM.
+- `@devframes/json-render-ui` — the official reference frontend library: a Vue renderer implementing the base catalog with `@antfu/design`. Any compatible frontend library can replace it.
 
-## 1. Package ownership and exports (resolves decision 03)
+A single view definition authored once renders standalone (the app supplies a compatible frontend lib) and inside a hub dock (the hub supplies the frontend lib), live or static.
 
-### `@devframes/json-render`
+## 1. Package relationship
 
-Location `packages/json-render/`. ESM, `tsdown`, own `tsconfig.json` (extends `tsconfig.base.json`), `build` + `typecheck` scripts, committed tsnapi snapshot. Two entry graphs kept separate so the node factory never pulls browser code:
+```
+devframe (core)          — no json-render awareness; a plain app pulls ZERO json-render deps
+   ▲ peer
+@devframes/json-render   — OPT-IN protocol layer; augments a devframe ctx (arrow is json-render → devframe only)
+   ▲ peer
+@devframes/json-render-ui — OFFICIAL / reference compatible frontend lib (others may be compatible)
+
+@devframes/hub           — json-render-AGNOSTIC; extensible dock types; registerRenderer() routes a dock
+                            type to a registered renderer; NO json-render dependency
+```
 
-| Subpath | Contents |
-| --- | --- |
-| `.` (isomorphic) | Protocol schema + types, base catalog, extension composition, action-declaration builder, validator, catalog-compatibility helper. Pure data + Zod; safe in node and browser. |
-| `./node` | `createJsonRenderView` and devframe integration helpers (touch RPC/shared-state/scope). Node only. |
-| `./core` | Curated named re-exports of `@json-render/core@0.19.0` (see below). |
+- **`devframe` core stays clean.** It never imports json-render. The current base-context doc comment that names `createJsonRenderer` as a host augmentation is removed. A devframe app that doesn't declare JSON-render views has no new dependency.
+- **`@devframes/json-render` is the opt-in layer.** It peer-depends on `devframe` and augments an existing `DevframeNodeContext` through the context's own scope / RPC / shared-state / views surfaces — it does not fork the context. Depends on `@json-render/core` (caret, §3) and `zod`.
+- **`@devframes/json-render-ui` is the reference frontend.** It peer-depends on `@devframes/json-render` and `vue`, depends on `@json-render/vue` (caret, paired with core), and dev-depends on `@antfu/design`. It is never a dependency of the protocol package — Vue stays out of the protocol.
+- **`@devframes/hub` gains no json-render dependency** (§5).
 
-**Curated core re-export (`./core`).** Explicit named list, never `export *`. Each name is a Devframes semver commitment; adding or removing one is a Devframes API change. Initial set, per the upstream study: `defineSchema`, `defineCatalog`, catalog inference types, `Spec`, `UIElement`, state/visibility/prop/action types and resolvers, `StateStore`, `createStateStore`. Excluded from the base: prompt/generation/edit/devtools hooks and all streaming primitives (may return later as an opt-in `./core/stream` subpath). `@json-render/core` is pinned to **exact `0.19.0`**; bumping it requires re-running the compatibility tests.
+### Deployment modes
 
-**Dependencies.** `@json-render/core` exact-pinned (catalog `inlined`), `zod ^4` aligned to core's instance (catalog `types`/`inlined`). `devframe` is a **peer** (the node helpers augment a devframe context; they never fork it). No dependency on `@devframes/hub`.
+- **Standalone.** The author opts in with `devframe` + `@devframes/json-render` + a compatible frontend lib (`@devframes/json-render-ui`, or another). The `dev` / `build` adapters and the `vite` helper serve that lib's SPA. devframe bundles no renderer by default.
+- **Hub-mounted.** The app provides `@devframes/json-render` (protocol + view); the **hub provides the frontend lib** to render, through its client-host `registerRenderer()` (default `@devframes/json-render-ui`). The app need not bundle a UI.
 
-**Type names.** `DevframeJsonRenderSpec`, `DevframeJsonRenderCatalog`, `JsonRenderView`, `JsonRenderViewRef`, `JsonRenderActionDeclaration`, `JsonRenderExtension`. These replace the hub's hand-written `JsonRenderSpec` / `JsonRenderer`.
+## 2. Package layout and exports
 
-### `@devframes/json-render-ui`
+### `@devframes/json-render`
 
-Location `packages/json-render-ui/`. Same monorepo contract plus a Storybook. Subpaths:
+`packages/json-render/`. ESM, `tsdown`, own `tsconfig.json`, `build` + `typecheck` scripts, committed tsnapi snapshot. Node and browser entry graphs kept separate:
 
 | Subpath | Contents |
 | --- | --- |
-| `.` | `createRenderer()` shell, base Vue registry, `defineRegistry`/`extendRegistry`, provider lifecycle wiring, loading/error surfaces. |
-| `./components` | Individual ported components for direct import / third-party reuse. |
-| `./spa` | Prebuilt standalone SPA entry assets (relative base) consumed by devframe adapters. |
-
-**Dependencies.** `@devframes/json-render` (peer + workspace), `@json-render/vue` exact-pinned `0.19.0` (paired with core), `vue ^3.5` (peer), `@antfu/design` (dev, build-time). Never a dependency of the protocol package — Vue stays out of the protocol.
-
-### Dependency direction
-
-```
-@devframes/json-render-ui ──▶ @devframes/json-render ──▶ devframe (peer)
-                                        │                 @json-render/core (pinned)
-@devframes/hub ─────────────────────────┘  (dock discriminator references JsonRenderViewRef)
-```
-
-The hub depends on `@devframes/json-render` only for the serializable `JsonRenderViewRef` used in its dock discriminator. It never imports the Vue UI.
-
-## 2. Protocol: catalog, state, actions (resolves decision 04)
+| `.` (isomorphic) | Base catalog + Devframes-authored per-component prop schemas, `JsonRenderViewRef`, and the re-exported protocol types. Pure data + Zod. |
+| `./node` | `createJsonRenderView` and devframe integration helpers (RPC/shared-state/scope). Node only. |
+| `./core` | Curated named re-exports of `@json-render/core` (below). |
 
-**Devframes owns the wire contract.** A `DevframeJsonRenderSpec` is validated against a Devframes-authored Zod schema, structurally compatible with core `Spec`/`UIElement` but not derived from upstream typing (the study shows upstream typing checks names, not per-component props or actions). Every spec carries `protocolVersion: 1` and `catalog: { id, version, extensions: [{ id, version }] }`.
+**`./core` re-export — builders + types only.** Explicit named list, never `export *`: `defineSchema`, `defineCatalog`, catalog inference types, `Spec`, `UIElement`, `StateStore`, `createStateStore`. Excludes streaming, prompt/generation, and devtools hooks. Each name is a Devframes semver commitment.
 
-**Base catalog v1** = the fourteen components inventoried in the seams study (`Stack`, `Card`, `Text`, `Badge`, `Button`, `Icon`, `Divider`, `TextInput`, `Switch`, `KeyValueTable`, `DataTable`, `CodeBlock`, `Progress`, `Tree`), with props/defaults normalized: documented props only, typed event payloads (`Button.press`, `TextInput.change`, `Switch.change`, `DataTable.rowClick` carry their value/row/index), and `language`/`loading` honored. Undocumented upstream props (`Stack.flex`, `Badge.title`/`minWidth`) are either promoted to documented props or dropped — each decided in the component port, not silently accepted. Catalog is versioned (`catalogVersion` independent of `protocolVersion`).
+**Type names.** `DevframeJsonRenderSpec` (alias of upstream `Spec`), `JsonRenderView`, `JsonRenderViewRef`. These replace the hub's hand-written `JsonRenderSpec` / `JsonRenderer`.
 
-**Extensions.** An extension is `{ id, version, components, actions }`. Composition is explicit and collision-on-duplicate is an **error** (no upstream last-write-wins). Base is the portable floor; `$computed` and custom directives, which execute host code, are extension-only capabilities, never base assumptions.
-
-**State.** `Record` addressed by JSON Pointer, JSON-serializable. Spec structure is replaced whole on update; **state updates travel as JSON-Pointer patches** over shared state (enable patches for this server-created state — they are currently disabled, per the seams study). This keeps state syncs narrow without a whole-spec broadcast.
+### `@devframes/json-render-ui`
 
-**Actions — declared allowlist, no catch-all bridge.** This is the central correction to the Vite implementation's arbitrary action-string→RPC proxy. Each view declares its actions up front:
+`packages/json-render-ui/`. Same monorepo contract plus a Storybook.
 
-```ts
-declareAction('reload', { params: z.object({ id: z.string() }) })
-```
+| Subpath | Contents |
+| --- | --- |
+| `.` | `createRenderer()` shell, base Vue registry, provider lifecycle wiring, action bridge, loading/error surfaces. |
+| `./components` | Individual ported components for direct import / reuse. |
+| `./spa` | Prebuilt standalone SPA entry assets (relative base) served by devframe adapters. |
 
-- Client dispatch: the action name must be in the view's allowlist and params must parse against the declared schema **before** any RPC crosses the boundary. Unknown names refuse to dispatch (no probing a Proxy).
-- Server: the RPC handler re-validates name + params at the trust boundary.
-- Reserved client-local built-ins (no RPC): `setState`, `pushState`, `removeState`, `validateForm`, matching upstream semantics.
+## 3. Upstream coupling
 
-**Serialization.** Strict JSON only for specs and state; the shared-state getter is marked `jsonSerializable: true` so the obligation is enforced, not assumed.
+- **Upstream is the wire contract.** A Devframes spec *is* an `@json-render/core` `Spec`; the Vue `schema` is accepted **as-is**. Devframes does not author a bespoke spec schema and does not extend the schema to validate the fields the Vue schema omits (`state`, `on`, `repeat`, `watch`) — those pass structurally unchecked.
+- **Per-component prop validation is the one validation Devframes adds.** Because upstream `defineCatalog` collapses multi-component `propsOf` to `Record`, Devframes authors a Zod prop schema per base component and parses element props against it at **both** boundaries: at spec ingress (server, in `createJsonRenderView` update — reject invalid props with a `DF` diagnostic) and at render time (client renderer, isolate a bad element).
+- **Caret range** on `@json-render/core` and `@json-render/vue` (`^0.19.0`, kept paired). No compatibility test suite; the committed **lockfile is the only guard** against a breaking upgrade. This is an accepted risk given the pre-1.0 upstream.
+- **Compatibility signal is the upstream package version.** No Devframes protocol/catalog version stamp.
+- **Streaming / generative-UI is out of v1.** A complete remote spec renders without it; it may return later behind an opt-in `./core/stream` subpath, reusing core's patch semantics.
+- **License.** Apache-2.0, compatible; keep required notices for any redistributed upstream object code and avoid implying Vercel endorsement.
 
-**Validation timing & failure.** Validate spec on the server at declaration and on every update; reject invalid specs with a `DF` diagnostic. Validate action name + params on the client before dispatch and again on the server. Browser-side render failures follow upstream's per-element isolation (render that element as null) and use `console.*`; node-side failures use `nostics`.
+## 4. Actions, state, and validation
 
-**Streaming / generative UI.** Excluded from v1. A complete remote spec renders without it. If adopted later, reuse core's patch semantics (core throws on failed RFC-6902 `test`; the Vue hook treats it as a no-op) behind an opt-in subpath, with patch validation and resource limits.
+- **Action bridge is unrestricted.** An element event maps to an action whose name is dispatched as an RPC call — any spec action string calls any RPC method the client can reach (the current `vitejs/devtools` behavior). There is no declared-action allowlist and no param validation. The capability implication — a spec, or anything that can write one to shared state, can invoke any client-reachable RPC — is accepted.
+- **Improve on the Vite bridge's quality.** Unlike the current proxy, the bridge tracks per-action loading state and surfaces RPC failures to the view rather than silently swallowing them to the console.
+- **State** is `Record` addressed by JSON Pointer, JSON-serializable. **State updates travel as JSON-Pointer patches** (enable patches on this server-created shared state, currently disabled); a structural change replaces the whole spec.
+- **Serialization.** Strict JSON only for specs and state; the shared-state getter is marked `jsonSerializable: true` so the obligation is enforced.
+- **Reserved client-local built-ins** (no RPC): `setState`, `pushState`, `removeState`, `validateForm`, per upstream semantics.
 
-## 3. Devframe runtime integration (resolves decision 05)
+## 5. Devframe runtime integration
 
-The JSON-render view is a **devframe-level** integration, not hub-only — it must run standalone. `createJsonRenderView` lives in `@devframes/json-render/node` and augments a base `DevframeNodeContext` through its existing scope/RPC/shared-state/views surfaces. The hub's `createJsonRenderer` is removed and its callers migrated.
+`createJsonRenderView` lives in `@devframes/json-render/node` and augments a base `DevframeNodeContext`:
 
 ```ts
-const view = createJsonRenderView(ctx, {
-  id: 'metrics',            // author-provided, stable
-  spec,
-  actions: { reload, clear },
-})
+const view = createJsonRenderView(ctx, { id: 'metrics', spec })
 view.update(spec)
 view.patchState([{ op: 'replace', path: '/count', value: 3 }])
 view.dispose()
 ```
 
-- **Identity.** Scoped stable IDs `devframe:json-render::`, replacing the global allocation-order counter. Stable across reconnects and rebuilds.
-- **Lifecycle.** `dispose()` unregisters the shared state and its listeners; no leak for the context lifetime.
-- **Scoped actions.** Registered as scoped RPC via `defineRpcFunction` under the namespacing convention, validated as in §2.
-- **Static.** Build dumps the spec + state as a shared-state query snapshot (read-only render). Actions are **not** dumped; a declared static policy renders action-bound controls in an explicit "unavailable in static output" state rather than silently inheriting live behavior. Local input bindings still work in the renderer.
-- **Standalone UI assets.** When a definition declares json-render views and the author ships no SPA, the `dev`/`build`/`spa` adapters serve `@devframes/json-render-ui/spa` (relative base, runtime connection discovery preserved). Author SPAs still win.
-- **View primitive.** Reuse the existing devframe `views` surface; the json-render view is a typed specialization, not a new primitive.
-
-## 4. Hub projection and renderer substitution (resolves decision 06)
-
-- **Serializable dock ref.** The dock discriminator becomes `{ type: 'json-render', view: JsonRenderViewRef }` where `JsonRenderViewRef = { stateKey, protocolVersion, catalog }` — no functions. This fixes the accidental `_stateKey` projection and the current type/projection mismatch where the projected `JsonRenderer` overstates what crosses the wire.
-- **Headless host.** `createDevframeClientHost` gains a `json-render` branch that resolves a renderer implementation from a host-registered registry (default `@devframes/json-render-ui`, overridable by the UI kit), checks catalog compatibility (`catalog.id`/`version`/extensions) before mounting, drives loading/error states, and disposes on dock deactivation (unsubscribing shared-state listeners the Vite viewer currently leaks). The hub package itself acquires no Vue.
-- **Migration.** `packages/hub/src/types/json-render.ts`, `defineJsonRenderSpec`, and `createJsonRenderer` move to `@devframes/json-render`; the hub re-exports `JsonRenderViewRef` for its dock type only. tsnapi snapshots for `@devframes/hub` update accordingly.
+- **Identity.** Scoped stable IDs `devframe:json-render::` (author-supplied, stable); a duplicate id within a scope raises a `DF` diagnostic. Replaces the current global allocation-order counter.
+- **Lifecycle.** `dispose()` unregisters the shared state and its listeners.
+- **Static output.** The build dumps the spec + state as a shared-state query snapshot (read-only render). Action RPC is not dumped; controls whose elements carry `on` handlers render **disabled** with an "unavailable in static output" affordance. Local state/bindings still work.
+- **Default UI.** No renderer is bundled into devframe. When a definition declares JSON-render views and the author has added a compatible frontend lib, the `dev` / `build` adapters and the `vite` helper serve that lib's `./spa` assets (relative base, runtime connection discovery preserved). The `spa` adapter is wired when `createSpa` lands. Under a hub the hub supplies the renderer instead.
+- **View primitive.** Reuse the existing devframe `views` surface; the JSON-render view is a typed specialization.
 
-## 5. Official Vue + Antfu Design UI (resolves decision 07)
+## 6. Official Vue + Antfu Design UI
 
 `@devframes/json-render-ui` ships:
 
-- **Base registry** implementing catalog v1's fourteen components, each ported to `@antfu/design` semantic tokens (`bg-base`/`color-muted`/`border-base`…), the shared UnoCSS preset stack, Phosphor `i-ph:*` icons, and dark mode via `.dark`. Reference behavior is the Vite catalog; conforming code replaces its hardcoded RGB palette and bespoke button/input styling.
-- **Intentional changes from the Vite catalog:** typed event payloads; honor `language` (syntax highlighting) and `loading`; **Decision** — replace runtime Iconify network fetch + DOMPurify `innerHTML` with build-time Phosphor icons mapped by name (removes per-render network access and an HTML-injection surface); provider state reset made explicit on view-identity/protocol change (remount/reset vs preserve).
-- **Renderer shell** wiring `JSONUIProvider` + `Renderer` with the Devframes action bridge (declared allowlist, not the catch-all proxy), seeding `spec.state`, and owning remount/reset semantics.
-- **Registry extension** via `defineRegistry`/`extendRegistry` for catalog extensions; third-party registries replace the whole registry.
-- **Standalone SPA** entry (relative assets, OS-preference dark flip) under `./spa`.
-- **Storybook** following the repo's one setup (co-located stories, `viteFinal` + `unocss/vite`, `.dark` toggle, `bg-base color-base` decorator) covering every component, loading/error, and a full sample view.
+- **Base registry** implementing catalog v1's fourteen components (`Stack`, `Card`, `Text`, `Badge`, `Button`, `Icon`, `Divider`, `TextInput`, `Switch`, `KeyValueTable`, `DataTable`, `CodeBlock`, `Progress`, `Tree`) on `@antfu/design` semantic tokens, the shared UnoCSS preset, and dark mode via `.dark`. Reference behavior is the Vite catalog; the hardcoded RGB palette and bespoke button/input styling are replaced by semantic tokens.
+- **Corrections from the Vite catalog:** typed event payloads (`Button.press`, `TextInput.change`, `Switch.change`, `DataTable.rowClick` carry their value/row/index); `CodeBlock` honors `language`; components honor `loading`; provider state reset is explicit (see below).
+- **Icons are fully dynamic.** The `Icon` component resolves whatever icon name a spec supplies at runtime (sanitized), with no preferred or bundled icon set. This is a deliberate, documented deviation from the repo's Phosphor-first icon convention, which applies to a surface's own chrome, not to spec-driven content icons.
+- **Renderer shell** wiring the upstream provider + renderer with the unrestricted action bridge (§4), seeding `spec.state`, and owning reset semantics: **reset provider state on view-identity or upstream-version change; preserve it across ordinary spec/state updates.**
+- **Registry replacement.** A third party replaces the whole registry (there is no incremental extension in v1, §7).
+- **Standalone SPA** under `./spa` (relative assets, OS-preference dark flip).
+- **Storybook** following the repo's one setup, covering every component, loading/error, and a full sample view.
 
-## 6. End-to-end prototype (resolves decision 08)
+## 7. Hub projection and renderer substitution
 
-Before the spec is frozen, build one disposable prototype (scratch under `examples/`, deleted after) exercising all seams: one view authored once; rendered standalone by the official Vue registry; mounted in a hub dock; re-rendered by a swapped third-party registry; updated through a declared live action and a state patch; emitted as a static snapshot. Its only output is confirmation (or correction) of ownership, naming, lifecycle, and configuration ergonomics feeding §7.
+- **The hub is json-render-agnostic.** Its dock union becomes **extensible/open** rather than hard-coding a `json-render` variant; the opt-in `@devframes/json-render` integration contributes the `json-render` dock type and its serializable ref. `@devframes/hub` carries **no** json-render dependency. This enlarges the migration: the current closed `DevframeViewJsonRender` variant and the `createJsonRenderer` factory are removed from hub.
+- **Serializable dock ref.** `JsonRenderViewRef = { stateKey, upstreamVersion }` — no functions, no Devframes catalog version. This fixes the accidental `_stateKey` projection and the current type/projection mismatch.
+- **`registerRenderer()` on `@devframes/hub/client`.** Configured at `createDevframeClientHost` boot; the host application injects `@devframes/json-render-ui` as the default. The client-host routes a dock type to its registered renderer, drives loading/error, and disposes on deactivation (unsubscribing shared-state listeners the Vite viewer leaks). A renderer/upstream-version mismatch logs a warning rather than blocking. The hub package itself acquires no Vue.
+- **Extensions deferred.** v1 supports the versioned base catalog plus whole-registry replacement. Incremental named extensions (add-on components with their own identity/version) are post-v1 — they clash with the upstream-version-only compatibility model and add protocol surface now.
 
-## 7. Implementation sequence
+## 8. Implementation sequence
 
 Each step is an independently reviewable slice; ⟶ marks the dependency.
 
-1. **Scaffold `@devframes/json-render`** — manifest, `tsdown.config.ts` (isomorphic/node/core graphs), `tsconfig.json`, `typecheck` script, empty tsnapi snapshot, catalog deps, exact core pin.
-2. **Protocol core** ⟶1 — spec Zod schema, base catalog v1, extension composition (collision = error), catalog-compatibility helper, validator, `DF` diagnostics. Unit tests.
-3. **Declared-action contract** ⟶2 — action builder, param validation, reserved built-ins, client + server validators.
-4. **Runtime factory** ⟶3 — `createJsonRenderView` (scoped IDs, shared state, JSON-Pointer state patches enabled, scoped action RPC, `dispose`), `jsonSerializable` getter.
-5. **Hub migration** ⟶4 — move types/factory out of hub, `JsonRenderViewRef` dock discriminator, correct projection, update hub tsnapi.
-6. **Scaffold `@devframes/json-render-ui` + base registry** ⟶2 — package, fourteen ported components, tokens, Storybook.
-7. **Renderer shell + action bridge** ⟶3,6 — provider/renderer wiring, declared-action bridge, loading/error, reset semantics.
-8. **Client-host projection** ⟶5,7 — `json-render` branch, renderer registry/injection, compatibility check, lifecycle/disposal.
-9. **Standalone adapters + SPA** ⟶4,7 — serve default SPA from `./spa`; static snapshot + static-action policy.
-10. **Prototype** ⟶8,9 — validate ergonomics; feed corrections back.
-11. **Test matrix** ⟶ across — see §8.
-12. **Docs + error pages** ⟶ across — package docs, `docs/errors/DFxxxx.md` for each new code.
-
-## 8. Test matrix (resolves the map's deferred slices)
-
-Behavioral coverage, since the seams study shows no current test touches `createJsonRenderer`, `_stateKey`, or a json-render dock:
-
-- Protocol: valid/invalid spec, per-component prop validation, extension collision, catalog compatibility.
-- Actions: allowlist enforcement, param schema pass/fail, reserved built-ins, server re-validation, rejection of undeclared names.
-- Runtime: scoped ID stability, `update`/`patchState`, disposal + listener cleanup, multiple concurrent views, reconnect.
-- Transport: whole-spec replace vs state patch, `jsonSerializable` enforcement, shared-state broadcast.
-- Hub: serializable dock projection (no functions), client-host renderer selection/injection, compatibility gate, deactivation disposal.
-- Standalone: live dev render; static build ordering (states registered before dump), static replay read-only, static-action policy.
-- Vue UI: each component's props/defaults/events/bindings (`press`/`change`/`rowClick`/`$bindState`), loading/error, reset-on-identity-change; Storybook render.
-- Snapshots: tsnapi for both packages and the updated hub; `tests/exports.test.ts` subpaths.
-
-## 9. Diagnostics
-
-New node diagnostics use `nostics` with sequential `DF` codes. `@devframes/json-render` is core-side protocol/runtime → allocate in the core range (next free after the current highest `DF00xx`); hub projection changes stay in the hub dock range (`DF81xx`). Suggested set: invalid spec, unknown/invalid action, catalog incompatibility, duplicate extension id, view id collision, disposed-view use. Each gets a `docs/errors/DFxxxx.md` page. Browser-only render failures keep `console.*`.
-
-## 10. Acceptance criteria
-
-- One view definition renders standalone (dev/build/spa/embedded) and in a hub dock, live and static, with the official Vue UI and a substituted registry.
-- No arbitrary action-string→RPC path exists anywhere; every action is declared and param-validated on both sides.
+1. **Scaffold `@devframes/json-render`** — manifest, `tsdown.config.ts` (isomorphic/node/core graphs), `tsconfig.json`, `typecheck`, empty tsnapi snapshot, caret core dep.
+2. **Protocol surface** ⟶1 — `./core` re-exports, base catalog + per-component Zod prop schemas, `JsonRenderViewRef`, `DF` diagnostics. Unit tests.
+3. **Runtime factory** ⟶2 — `createJsonRenderView` (scoped IDs, shared state, JSON-Pointer state patches enabled, ingress prop validation, `dispose`), `jsonSerializable` getter.
+4. **Hub decoupling** ⟶2 — open the dock union, remove `createJsonRenderer` + the closed json-render variant from hub, add `registerRenderer()` to the client host, regenerate hub tsnapi.
+5. **Scaffold `@devframes/json-render-ui` + base registry** ⟶2 — package, fourteen ported components (dynamic `Icon`), tokens, Storybook.
+6. **Renderer shell + action bridge** ⟶2,5 — provider/renderer wiring, unrestricted bridge with loading/error, render-time prop validation, reset semantics.
+7. **Client-host routing** ⟶4,6 — dock-type → registered renderer, default injection, disposal.
+8. **Standalone adapters + SPA** ⟶3,6 — serve the author's frontend lib `./spa`; static snapshot + static-action affordance.
+9. **Prototype** ⟶7,8 — one disposable end-to-end prototype (one view: standalone + hub dock + swapped registry + live update + static snapshot); feed ergonomics back before finalizing.
+10. **Test matrix** ⟶ across — §9.
+11. **Docs + error pages** ⟶ across — package docs, `docs/errors/DFxxxx.md` per new code.
+
+## 9. Test matrix
+
+- Protocol: per-component prop validation (ingress + render), catalog membership.
+- Runtime: scoped ID stability, `update` / `patchState`, disposal + listener cleanup, multiple concurrent views, reconnect.
+- Transport: state-patch vs whole-spec replace, `jsonSerializable` enforcement, shared-state broadcast.
+- Actions: bridge dispatch, per-action loading, error surfacing.
+- Hub: extensible dock projection (no functions on the wire), client-host renderer routing/injection, version-mismatch warning, deactivation disposal.
+- Standalone: live dev render; static build ordering (states registered before dump), static replay read-only, static-action affordance.
+- Vue UI: each component's props/defaults/events/bindings (`press`/`change`/`rowClick`/`$bindState`), dynamic icon resolution, loading/error, reset-on-identity/version-change; Storybook render.
+- Snapshots: tsnapi for both new packages and the updated hub; `tests/exports.test.ts` subpaths.
+
+## 10. Diagnostics
+
+New node diagnostics use `nostics` with sequential `DF` codes. `@devframes/json-render` protocol/runtime → the core range (next free after the current highest `DF00xx`); hub dock changes stay in the hub dock range (`DF81xx`). Set: invalid element props (ingress), duplicate view id, disposed-view use, catalog/registry error. Browser-only render failures keep `console.*`.
+
+## 11. Acceptance criteria
+
+- A plain devframe app pulls no json-render dependency; JSON-render is added only by depending on `@devframes/json-render`.
+- One view definition renders standalone (the app's frontend lib) and in a hub dock (the hub's registered renderer), live and static.
+- `@devframes/hub` and `devframe` acquire no Vue and no json-render dependency respectively; the frontend lib is fully replaceable via `registerRenderer()`.
 - Dock projection carries only serializable data; no function survives to the wire, and the projected type matches what the client receives.
 - Views have stable scoped IDs and dispose cleanly with no leaked shared state or listeners.
-- The hub and devframe packages acquire no Vue; the Vue UI is fully replaceable.
+- Element props are validated at ingress and render; the action bridge surfaces loading and errors.
 - `pnpm lint && pnpm test && pnpm typecheck && pnpm build` pass, with tsnapi snapshots and exports tests updated.
 
 ## Out of scope
 
 - Implementing, publishing, or releasing the packages (this plan is the specification).
 - Changing `vitejs/devtools` to consume the packages or deleting its current implementation.
-- Official React/Solid/Svelte/vanilla registries — third-party compatibility is specified through the core contract only.
-- Agent-driven / generative UI beyond keeping the base catalog compatible with json-render.
+- Official React/Solid/Svelte/vanilla frontend libs — other frontends are supported through registry replacement, not shipped here.
+- A declared-action allowlist, a Devframes protocol/catalog version, incremental named catalog extensions, and streaming — each explicitly deferred or declined above.