diff --git a/plans/json-render/plan.md b/plans/json-render/plan.md new file mode 100644 index 00000000..7491123c --- /dev/null +++ b/plans/json-render/plan.md @@ -0,0 +1,169 @@ +# JSON Render Packages — Implementation Plan + +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. 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. + +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). + +## Destination + +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: + +- `@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. + +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. + +## 1. Package relationship + +``` +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 +``` + +- **`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). + +### Deployment modes + +- **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. + +## 2. Package layout and exports + +### `@devframes/json-render` + +`packages/json-render/`. ESM, `tsdown`, own `tsconfig.json`, `build` + `typecheck` scripts, committed tsnapi snapshot. Node and browser entry graphs kept separate: + +| Subpath | Contents | +| --- | --- | +| `.` (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). | + +**`./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. + +**Type names.** `DevframeJsonRenderSpec` (alias of upstream `Spec`), `JsonRenderView`, `JsonRenderViewRef`. These replace the hub's hand-written `JsonRenderSpec` / `JsonRenderer`. + +### `@devframes/json-render-ui` + +`packages/json-render-ui/`. Same monorepo contract plus a Storybook. + +| 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. | + +## 3. Upstream coupling + +- **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. + +## 4. Actions, state, and validation + +- **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. + +## 5. Devframe runtime integration + +`createJsonRenderView` lives in `@devframes/json-render/node` and augments a base `DevframeNodeContext`: + +```ts +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::` (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. + +## 6. Official Vue + Antfu Design UI + +`@devframes/json-render-ui` ships: + +- **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. + +## 7. Hub projection and renderer substitution + +- **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. + +## 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`, 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. +- 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 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. 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.