# Pygmalion

[English](./README.md) | [Korean](./README.ko.md)

Pygmalion is a code-backed visual editor for React applications. It renders real
DOM and registered React components on a canvas, keeps shared component
instances synchronized, and connects visual edits to reviewable source changes.

> **Why Pygmalion?** The name comes from the myth of a sculptor whose creation
> came to life.
>
> **Behind every design is your React code.**

## What you can do

- Browse registered screens, components, assets, and design tokens.
- Navigate large screen sets from the sidebar or canvas.
- Edit layout, styles, text, props, and supported component structure.
- Choose between a shared-source edit and a single-screen override.
- See shared edits update every affected frame immediately.
- Inspect the source diff and affected screens before applying a change.
- Compare baseline and changed screenshots with DOM and pixel QA.
- Undo or redo canvas edits, or reset every uncommitted editor change.

## Install

Pygmalion is distributed as the public npm package
`@pygmalionjs/pygmalion`. No registry configuration or authentication token is
required.

```bash
npm install --save-dev @pygmalionjs/pygmalion
```

## Configure a host project

Create `pygmalion.config.ts` in the host application:

```ts
import path from "node:path";
import { definePygmalionProject } from "@pygmalionjs/pygmalion/vite";

const appRoot = import.meta.dirname;
const projectRoot = path.resolve(appRoot, "..");

export default definePygmalionProject({
  configRoot: appRoot,
  projectRoot,
  appRoot,
  appDirectory: "frontend",
  sourceDirectory: "src",
  source: { remote: "origin", branch: "dev" },
  mirrorRoot: path.resolve(projectRoot, "..", "app-dev-view"),
  inventory: {
    script: path.join(appRoot, "scripts", "generate-pygmalion-inventory.mjs"),
    outputRoot: appRoot,
    outputs: [
      "src/generated/design-registry.ts",
      "src/generated/design-inventory.ts",
    ],
    runtimeInputs: [".env.local"],
  },
  inspect: {
    normalizeValue(property, value) {
      return value;
    },
  },
});
```

`inventory.outputs` lists generator-owned paths relative to the application root. Pygmalion
restores these paths in its dedicated mirror before switching revisions and checks again after
generation; any change outside the declared outputs stops synchronization.
Git does not report ignored files, so list ignored runtime or configuration
files a generator could affect in `inventory.runtimeInputs`. Each entry is one
file relative to the application root, with at most 64 files and 16 MiB total.
Pygmalion compares their content directly before and after generation; it does
not recursively scan `.git`, `node_modules`, or other unrelated directories.

Add the integration plugins to the host Vite configuration:

```ts
import { createPygmalionVitePlugins } from "@pygmalionjs/pygmalion/vite";
import pygmalionConfig from "./pygmalion.config";

export default defineConfig({
  plugins: [react(), ...createPygmalionVitePlugins(pygmalionConfig)],
});
```

Frameworks that expose webpack loaders through a JavaScript-only pipeline can
ask the inspect loader to strip TypeScript after source instrumentation. A
Turbopack rule should set `transpile: true` while keeping the original file
extension, so relative TSX module resolution keeps the host's source identity:

```ts
{
  './src/**/*.tsx': {
    loaders: [{
      loader: require.resolve('@pygmalionjs/pygmalion/webpack-loader'),
      options: { root: process.cwd(), sourceDirectory: 'src', transpile: true },
    }],
  },
}
```

When the mirror preview reloads the host Vite configuration,
`PYGMALION_PREVIEW_MODE=1` prevents the integration plugins from starting
recursively.

Mirror and session previews run as their own Vite processes, so the mode you
pass to the editor does not reach them. Set `preview.mode` when a screen only
exists under a specific env file — a feature flag that has to be off, a mock
profile a recipe depends on:

```ts
export default definePygmalionProject({
  // ...
  preview: { mode: "e2e" },
});
```

## Switch what the mirror renders

The mirror follows `source.branch` unless `source.ref` pins it. Either can be
repointed at runtime, so comparing two revisions no longer means restarting the
dev server:

```ts
const { switchSource, mirror } = usePygmalionProject();

await switchSource("origin/dev");
await switchSource(PYGMALION_WORKTREE_SOURCE_REF); // uncommitted work
mirror.sourceRef; // what it is tracking now
```

`PYGMALION_WORKTREE_SOURCE_REF` renders the current working tree by resolving it
to a throwaway commit, leaving your branch, HEAD, and index untouched. It covers
tracked changes only; untracked files raise a warning on the returned status.

### Bind screen declarations to the selected revision

An editor host is normally built once, while its mirror can switch among many
checkouts. Bundling routed pages into the host therefore lets a new preview run
under an old screen catalog. Generate a JSON catalog inside every mirror
checkout and let the exact-source endpoint serve it under a read lease:

```ts
export default definePygmalionProject({
  // ...
  inventory: {
    script: './scripts/generate-pygmalion-inventory.mjs',
    outputs: ['artifacts/pygmalion-revision-catalog.json'],
  },
  preview: {
    catalog: { file: 'artifacts/pygmalion-revision-catalog.json' },
  },
});
```

With the default `inventory.script` invocation, Pygmalion passes
`--source-root <path> --out-root <path> --commit <sha>`. A custom
`inventory.args(context)` receives the same exact `context.commit` when the
generator uses another CLI shape.

The generated file uses the versioned, JSON-only contract exported as
`PygmalionRevisionCatalog`. Stamp `sourceRevision` from the inventory
generator's `commit` context and include routed `screens.pages`,
`screens.assets`, `tokens`, and every screen declaration that can differ by
revision. Put serializable registry metadata in `componentRegistry`; the editor
binds it by name to the React implementations already bundled by the host and
rejects a catalog whose implementation is unavailable. This keeps props,
defaults, adoption, and code-generation metadata revision-bound without putting
functions in JSON.
Repository-owned generated-code intent belongs in `codegenProject` so its
frame contracts and prompt provenance switch with the same catalog.
Then opt the editor into the exact-preview services through its transport:

```tsx
// Once, for the editor's lifetime. The catalog endpoint derives from the
// artifact endpoint; pass `previewCatalog: null` to run artifacts alone.
const transport = createViteTransport({ endpoints: { previewArtifact: true } });
const runtime = createEditorRuntime({ transport });

function Editor() {
  const { appOrigin, previewRevision } = usePygmalionProject({ transport });
  return (
    <PygmalionEditor
      runtime={runtime}
      previewCacheNamespace="my-product"
      previewRevision={previewRevision}
      appOrigin={appOrigin ?? undefined}
    />
  );
}
```

Every request the editor and the project hook make goes through that
transport, so the two talk to one service; [The editor transport](./docs/transport.md)
lists its operations, paths, and errors. Pass the hook's full
`previewRevision`; the editor derives its source SHA for the catalog request
while retaining the session mutation counter for screen cache invalidation.

The editor validates both namespace and source revision before installation.
While another checkout is preparing, it keeps the last complete catalog,
preview origin, and interaction declarations together. A late response from an
older dev → qa → dev request cannot overwrite the current selection.
Invalid, permanently unavailable, or catalogs that produce no installable page
fail closed on a dedicated error surface; the editor never relabels the
previous canvas as the requested revision. A catalog with no routed screens
remains installable when the host contributes non-screen library pages.

When `onDesignChange` is connected, each payload carries the exact
`sourceRevision` that owned the edited batch. Its non-enumerable `signal` is
aborted when that catalog is retired, allowing an in-process host adapter to
cancel work without changing an existing JSON request body.

To offer a choice instead of asking for a ref string, list the revisions and
render the shipped control in your toolbar:

```tsx
const { sourceRefs, switchSource, mirror } = usePygmalionProject();

<SourceRefControl
  value={mirror.sourceRef}
  currentCommit={mirror.shortCommit}
  refs={sourceRefs.refs}
  defaultRef={sourceRefs.defaultRef}
  worktreeRef={sourceRefs.worktreeRef}
  busy={mirror.state === 'syncing'}
  refreshLabel="Refresh this branch"
  onChange={(ref) => void switchSource(ref)}
>
  {/* Optional: your own trigger, so the control reads as part of the toolbar. */}
  <span>{mirror.sourceRef ?? 'dev'}@{mirror.shortCommit}</span>
</SourceRefControl>;
```

The menu is a native select layered over that trigger, so keyboard access,
mobile behavior, and placement stay with the platform while the painted trigger
stays yours. Its first item explicitly reloads the selected revision, including
when a branch tip moved while that same branch remained selected. Passing
`currentCommit` marks the control with `[data-update-available]` and
`[data-latest-commit]` when the listed ref is newer. `[data-pygmalion-source-ref]`
and `[data-busy]` are also available for styling.

`GET /__pygmalion-dev-control/refs` backs this list with local branches, remote
tracking branches, the default revision, and the worktree ref. `loadSourceRefs()`
reloads it after branches change.

Preview artifacts are cached per source SHA, so switching invalidates that cache
and the next capture runs again from scratch. A capture already running for the
previous revision is discarded instead of published, so it cannot overwrite the
artifact the editor moved to.

A design session's worktree is branched from one source SHA. Once it holds
unapplied edits, `switchSource` refuses rather than leave the canvas on a
revision those edits were never written against — apply or revert them first, or
pass `{ discardEdits: true }`.

## Check a frame at the widths you support

A frame resizes by corner drag or by the W/H boxes. Name the widths your
application actually guarantees and they become one-click buttons next to those
boxes, so moving between them costs nothing to remember:

```tsx
<PygmalionEditor
  viewportPresets={[
    { label: '480', width: 480, height: 800, description: 'Minimum supported width' },
    { label: 'Default', width: 1280, height: 800 },
  ]}
/>
```

Omit `height` to change width alone and leave the frame's height as it is. The
button reads as active while the frame sits at that size, and **Reset to default
size** still returns the frame to the viewport its manifest authored.

If the first declared canvas is a large asset catalog, open the working canvas
directly and avoid rendering that catalog during startup:

```tsx
<PygmalionEditor initialCanvas="Screens" />
```

### Screens that never stop moving

A capture waits for the document to hold still. A screen with an elapsed clock, a
level meter, or a marquee never does, so it fails on `stabilize` no matter how
long it waits. Declare those regions and their mutations stop counting as churn:

```ts
captureStoryboardCase({
  // ...
  stability: {
    volatileSelectors: ['[data-testid="elapsed-time"]', '[data-live-meter]'],
  },
});
```

The wait itself samples every 100ms and needs three identical samples, up to a
4s ceiling — wide enough for a screen that repaints on a one-second beat.
`attempts`, `requiredStableSamples`, `intervalMs`, and `minimumWaitMs` are
adjustable for anything slower.

## Register screens and scenarios

The host owns application-specific routes, fixtures, authentication, mock data,
and screen names. Pygmalion owns preview scheduling, DOM capture, editing,
source sessions, and QA.

Use `createDesignScreenCollection` to classify every discovered scenario as
either:

- a visual screen with a route or registered component fixture; or
- a non-visual behavior with an explicit reason.

A scenario must appear in exactly one category. Invalid references, missing
coverage, and duplicate classifications fail before the editor opens.

Whether every source route is represented is a separate question, answered
headless: `auditRouteCoverage` compares the routes the host discovered with
the screens it declared (a concrete preview, an explicit `coverageRoutes`
mapping, or a registered component fixture), `validateSurfaceDemands` checks
the verdicts for the rest against the declarations they name, and the
summaries count exclusions and todo items apart from coverage.
[Coverage contracts](./docs/coverage-contracts.md) defines the identities,
the diagnostics, and what stays in the host.

Screen recipes support `click`, `focus`, `hover`, `focus-visible`, `active`,
`fill`, `check`, `press`, `scroll`, `wait`, and `storage` steps. Use deterministic recipes
and assertions so each frame represents a repeatable application state. The
[screen state contract](./docs/screen-state-contract.md) defines when a
variation is a frame, an interaction state, a condition state, an editable
parameter, a motion preview, or behavior-only QA. Rendered CSS and Web
Animations targets are discovered and settled for captures; a live frame
plays the product's own motion.

### Keep product routes separate from preview entry routes

Authentication and fixture gateways belong to the host application. When a
screen needs one, keep `route` as the canonical product address and declare the
routed gateway as `previewRoute`:

```ts
{
  id: 'screen:account-security',
  name: 'Account security',
  section: 'Account',
  route: '/account/security',
  previewRoute: '/__preview__/account/security',
}
```

Pygmalion boots, captures, caches, and interacts through `previewRoute`, while
inventory, coverage, and source identity continue to use `route`. The gateway
should be development-only, keep credentials out of URLs and client manifests,
and render or redirect to the real routed screen rather than a separately
assembled component fixture. Changing the gateway invalidates that frame's
preview cache automatically.

Declared `screenFlows` support the same split. Put the canonical starting
address in `route` and the host gateway in `previewRoute`; the flow boots the
gateway and still reports the canonical route to capture preparation.

Keep user journeys explicit instead of relying only on source navigation
inference. Paths expand into adjacent graph edges, and source-discovered edges
are still merged underneath them:

```tsx
const storyboard = {
  startScreenIds: ['screen:entry'],
  paths: [
    {
      id: 'checkout',
      label: 'Checkout',
      screenIds: ['screen:entry', 'screen:cart', 'screen:confirmation'],
    },
  ],
};

<PygmalionEditor storyboard={storyboard} />
```

Every id is checked against the resolved screen catalog. Unknown endpoints and
unreachable screens stay visible as graph diagnostics instead of disappearing
from the sidebar. Code-only redirects or fixtures can be classified through
`nonVisualScenarioIds` with ids created by
`createStoryboardRouteScenarioId(route)`.

The editor shell and frame geometry mount before storyboard discovery finishes.
Cached or live route content remains gated until baseline environment and source
digests settle, so immediate canvas paint cannot consume a provisional preview
identity. Selecting an already visible frame in the sidebar preserves the
camera; an offscreen frame is revealed with the smallest pan possible and never
changes the user's zoom.

### Declare catalog conditions

A product is often several products at once: a color scheme, an account level
or a feature entitlement decides which screens exist and how every one of them
looks. A session preset is one runtime choice for the whole catalog, and a
preview environment control only dresses the live preview. Independent flags
are condition axes:

```tsx
const conditionAxes = [
  {
    id: 'theme',
    label: 'Theme',
    defaultValue: 'authored',
    options: [
      { value: 'authored', label: 'Auto' },
      { value: 'dark', label: 'Dark', environment: { localStorage: { 'app:theme': 'dark' } } },
    ],
  },
  {
    id: 'tier',
    label: 'Plan tier',
    options: [{ value: 'basic', label: 'Basic' }, { value: 'plus', label: 'Plus' }],
  },
];

<PygmalionEditor conditionAxes={conditionAxes} />
```

The Conditions panel lists every axis. The effective value is the explicit
choice, then `defaultValue`, then nothing: an axis without a default constrains
nothing until chosen, the same way the Default session shows every runtime.

Each option's `environment` joins the storyboard baseline underneath every
frame's own declared environment, in declared axis order and on top of the
selected session preset. Capture fingerprints, flow sessions and Application
runs already derive from that baseline, so choosing a value re-resolves the
catalog under it instead of reusing frames captured under the previous one. An
option without an environment is a pure catalog choice and leaves existing
capture identities intact; a default with an environment participates from the
start.

A screen names the values it exists under with `conditions`; an axis it does
not name means every value:

```tsx
{ id: 'billing', route: '/billing', conditions: { tier: ['plus'] } }
```

Unknown axis ids or values never widen the catalog: the revision catalog
validator rejects them, and at runtime such a screen is hidden and reported by
`conditionDeclarationIssues`. Frame-level condition axes from
`screenStateAxes` stay frame states and are not promoted to catalog filters. The
core evaluates the declarations; which product screens exist under which values,
and what each value means to the application, remain host declarations.

## Capture artifact lifecycle

Import the reusable capture primitives from
`@pygmalionjs/pygmalion/storyboard`. Pygmalion owns isolated browser contexts,
bounded scheduling, inert DOM snapshots, screenshots, QA diagnostics, compact
versioned artifacts, exact revision validation, and atomic artifact
publication. The Vite integration serves only an artifact whose namespace and
source revision match the active editor.

The host application supplies routes, environment presets, mock adapters,
interaction recipes, and assertions. A host hook may install domain-specific
network fixtures, but it must not fork the browser or artifact lifecycle.

Artifact v3 preserves every registered frame with one explicit outcome:
`ready`, `rendered-with-qa-failure`, or `capture-error`. QA failures can retain
their last stable DOM and screenshot while CI remains red. A failed strict
capture therefore cannot silently leave an older artifact active.

When the transport configures a preview artifact endpoint, an embedded
artifact with another namespace or source revision is treated only as a
visibly outdated, last-known preview. It does not count toward `data-preview-warm`. Each frame exposes its
exact-resolution lifecycle through `data-artifact-resolution`: `checking`,
`outdated`, `generating`, `error`, or `exact`. Transport, validation, and
generation failures stay visible on the frame with a retry action. A current
live snapshot or exact endpoint response clears the stale state.

## Editing workflow

For Icons, Atoms, and Components, select an item in **Assets** to open its
catalog frame and select the source implementation automatically. The trailing
`+` action remains insertion-only, so opening an asset never mutates another
frame.

1. Open **Screens**. Preview warm-up starts without requiring frame clicks.
2. Select a frame from the sidebar to center it on the canvas.
3. Select a DOM layer or registered component.
4. Choose **Shared component** or **This screen** as the edit scope.
5. Change layout, content, props, or supported structure.
6. Review the affected frame list and generated source diff.
7. Run visual QA, then apply the change to the isolated design session.
8. Use **Reset changes** to discard all unapplied edits and editor history.

Shared edits update detailed active frames and lightweight inactive frames in
the same render cycle. A frame does not need to be activated before it reflects
a shared change.

### Preserve QA review evidence locally

QA capture keeps its existing in-memory behavior by default. To recover baseline,
changed, and heatmap PNGs after a server restart, configure a local storage root
and a project namespace. The host must resolve each capture URL to an existing,
immutable session snapshot; a URL string alone is not source evidence.

```ts
import { createQaCaptureService } from '@pygmalionjs/pygmalion/qa';

const qa = createQaCaptureService({
  captureRunner,
  storage: { root: '/private/project-cache/qa', namespace: 'project:example' },
  resolveIdentity: async (request, { signal }) => {
    const snapshot = await sessions.resolveCapture(request.basePath, { signal });
    return { sessionId: snapshot.sessionId, sourceRevision: snapshot.sourceRevision,
      revision: snapshot.revision };
  },
});
await qa.ready(); // Needed before synchronous getArtifact calls after restart.
// capture() waits for restoration automatically.
const saved = await qa.readReview(reviewId, { sessionId, sourceRevision, revision });
```

The same options work with `pygmalionQaCapturePlugin` or `createPygmalionVitePlugins`
under `qa`. Successful persistent captures include `reviewId`, `reviewUrl`, and
`expiresAt` in the changed response. The plugin restores the existing PNG URLs
and serves the review JSON only through its local-host endpoint. Read-back
requires the exact project namespace, session, source revision, and snapshot
revision. Restored evidence never starts an iframe, recaptures a screen, or grants
source-write or proposal approval.

Each review retains its own before PNGs so it can outlive its baseline. Both
manifests and image bytes are hashed, and a complete directory is published
atomically. Linked paths and damaged records are rejected. The existing baseline
and artifact TTL, count, and byte limits also bound disk retention; persistent
expiry is fixed at capture time and is not renewed by reads or restart. The review
artifact budget includes its before, after, and heatmap PNGs. Storage failures or
an over-budget review fail explicitly without returning a saved-review link.
Capacity eviction can remove an older review before its TTL, including immediately
after another process publishes a newer review. Read-back then returns `null` or
HTTP 404; it never substitutes another capture. The resolver is trusted at capture
time. Historical read-back checks the saved identity without requiring the old
runtime session to remain active, and therefore remains evidence rather than a
current execution or publication approval.

### Connect source adapters to SDK services

The host supplies screen declarations, source mappings, authentication or mock
setup, and build commands. Pygmalion owns reusable execution, revision caching,
source review, and cancellation. Keep SDK implementation and regression tests
in Pygmalion; host tests continue to cover the product's own behavior.

| Package subpath | Responsibility |
| --- | --- |
| `./host-runtime` | Process-group cleanup, revision catalog queues and caches, isolated source roots, and lockfile-matched dependencies. |
| `./source-service` | Source and token review receipts, apply validation, reconciliation sessions, and local JSON transport. |
| `./source-reconciler` | Reconcile draft structure with external source edits using host-supplied source identity mappings. |
| `./draft-entry` | Generate a browser entry that applies the declared draft recipe and reports readiness or failure. |
| `./source-module` | Evaluate a trusted declaration bundle using the host's bundler configuration. |

For example, declare a build command in the host and let the SDK wait for its
process group to stop before an archive can be removed:

```js
import { executeWorkspaceDraftProcess } from '@pygmalionjs/pygmalion/host-runtime';

export function buildPreview({ cwd, signal }) {
  return executeWorkspaceDraftProcess(process.execPath, ['scripts/build-preview.mjs'], {
    cwd,
    signal,
    timeout: 120_000,
  });
}
```

Process-group cleanup supports macOS and Linux. Catalog generation callbacks
receive an isolated archive for the exact requested commit; cancelling one
catalog request does not cancel generation shared with another caller.

### Prepare declared text faces

An optional `WorkspaceTargetConfiguration.textFontPreparation` capability can
prepare fonts for the visible page and selected text layer. Create it with
`createTextFontPreparationRuntime(resolver)` and reuse it for the lifetime of
the declared font environment. The resolver receives the original family
expression, numeric weight, normal/italic style, exact text sample and an abort
signal. A ready result identifies the actual face and attests to that sample's
coverage; otherwise it returns a named unavailable or failed result.

With `capturedScreens` enabled, imported drafts inherit the root configuration's
`textFontPreparation` and `textRasterFaceResolver`. Explicit target loaders keep
their own capabilities; source and data mappings are never inherited by captured
drafts. Imports retain zero tracking and logical LTR alignment for text measurement.
Saved imports with the older omitted-zero tracking are normalized
when reopened, preserving their edits and leaving storage untouched until save.

The SDK shares identical pending requests, cancels abandoned readers, caches
successful results and offers retry without editing the document. Page changes
exclude hidden pages, and resolved component overrides retain their own font
requirements. Connected or source-rendered text needs an explicit rendered
sample; the SDK does not guess that sample from a canonical placeholder.

The existing family/weight `TextFontLoader` API remains available. Browser
`document.fonts.check()` success alone does not establish an exact face or glyph
coverage. The preparation capability does not download fonts implicitly or change
text geometry. Dispose the runtime when its font environment is no longer used.

### Export authored text and inspect file styles

The browser `createDesignRasterRenderer({ textFontPreparation, resolvePreparedFace })`
factory reuses that explicit preparation runtime for a selected static text
layer. The face resolver maps the prepared environment and face identity to its
exact registered `FontFace` object. Missing, replaced or ambiguous registrations
are refused. Workspace connects `textFontPreparation` and
`textRasterFaceResolver` to its inspector and asset export controls. Text
requires a fixed positive box, one unwrapped paragraph, explicit family, size,
weight, line height and zero letter spacing, one solid color, and a registered
browser face matching the declared style and weight. The provider attests exact
face identity and sample coverage; browser fallback or synthesized styling is
not accepted as that evidence.

`RasterExportRenderer.preflight` optionally provides a synchronous eligibility
check without rendering, mutation, or resource loading; custom renderers without
it retain their existing support contract. The inspector shows known export
limitations before activation, including the built-in renderer's current lack of
support for text inside a frame.

An exact full face can coexist with earlier CSS subsets of identical family,
weight, style and stretch when its last registration wins for the entire sample.
A later competing registration or different matching descriptors remain ambiguous.

PNG, JPG and WebP retain the layer box and existing output size limits. Text
with mixed runs, automatic sizing, layout-dependent placement, source/bound
content, decorations, effects or unresolved colors is refused. SVG text and
frames containing text remain unsupported by the built-in exporter. The
existing `renderDesignRaster` geometry renderer remains available without a
font provider. Raster success never enables a source writer.

`exportDesignNode` accepts an optional fifth `{ signal }` argument. Workspace
cancels pending output when the layer, document, target, page or mode changes,
including a rapid departure and return before a render. Export never adds a
history entry or applies implementation source.

With no layer selected, the properties sidebar lists the file's authored text
styles, grouped by their slash-separated names. Expanding a style displays its
stored typography and library origin. This metadata browser neither discovers
installed fonts nor downloads faces; selecting a layer retains the existing
style application and editing controls.

### Deploy uploaded fonts with project source

Browser uploads remain local until a host connects `fontSource`. The project
action reviews a complete CSS/SCSS and JSON-manifest diff before applying or
proposing it. The service embeds the exact font bytes in an existing imported
stylesheet, so deployment does not depend on IndexedDB or an upload server.

```ts
import { createProjectFontSourceService, sourceServicePlugin } from '@pygmalionjs/pygmalion/source-service';

const fonts = sourceServicePlugin({
  endpoint: '/__project-fonts',
  operations: ['list', 'prepare', 'apply'],
  createService: () => createProjectFontSourceService({
    appRoot,
    stylesheetFile: 'src/styles/project-fonts.css', // Already imported by the app.
    manifestFile: 'src/styles/project-fonts.json',
    assertTarget, // Validate the selected screen and exact source revision.
    canApply: true,
  }),
});
// Add fonts.plugin to the development server's plugins.
```

```ts
import { createProjectFontSourceClient } from '@pygmalionjs/pygmalion';

const fontSource = createProjectFontSourceClient({
  scope: 'project:example', endpoint: '/__project-fonts', canApply: true,
});
// Return fontSource on each supported WorkspaceTargetConfiguration.
```

The stable scope isolates each project's browser font library. Without a source
capability, the project action is disabled and uploads remain browser-local.
Proposal-only hosts use `canApply: false`, expose `propose`, set the client's
`canPropose: true`, and connect `proposePlan` to their existing Draft-PR publisher.
For other revisions, provide `archive` using `withSourceArchive`; preparation
and proposal proof checks then read the selected commit. The callback receives
the exact reviewed file plan and must never merge or write a base branch.

The service validates font metadata and bytes, confines both source paths to
the project, and rejects linked paths. Font files are limited to 20 MB and the
combined source plan to 32 MB. Matching faces are reused; replacements preserve
unrelated CSS and fonts. Ten-minute review receipts bind the target and file
proofs and are consumed by the first writer attempt. Failed or expired reviews
must be prepared again. Removing a browser font does not delete project source.

### Connect captured layers to implementation data

A host can return `WorkspaceDataSourceMapping` with a captured source baseline.
It declares the catalog, exact source expression proof, target slot, and
available field expressions. `mapCapturedDataTargets` in the `binding-source`
entry maps an explicit JSX attribute anchor to captured source provenance;
sampled text is never used to guess a source location.

`compileWorkspaceDocumentSource` returns a `bindingPlan` for declared text,
component inputs, visibility conditions, and repeated collections. A host supplies `compileSource` and uses
`compileSourceBindingFilePlan` to produce the ordinary complete-file plan. The
compiler validates source hashes, complete expression ranges, lexical symbol
availability, allowed files, and nullable fallback policy. Fixture values are
never written into implementation files. Review, isolated preview and proposal
use the same revision-bound receipt as other source edits.

Mixed edits use `compileSourceDocumentPlan` from `/source-structure`. Pass the
complete original `files` and hashes, `allowedFiles`, and the `visualPayload`,
`bindingPlan`, and optional `{ anchors, operations, wrapper }` structure returned
by the document compiler. It validates text, props and data against the same
original source, updates containing JSX anchors, and applies hierarchy changes
before compiling stylesheet edits in isolation. The final plan retains original
file proofs. Overlapping expression edits or rewrites that consume an owned
JSX boundary are blocked atomically; source ranges are never guessed from text.

`mapCapturedStyleTargets` connects explicit style declarations only when the
captured CSS-module owner, own-source JSX stamp, unshadowed stylesheet import,
and one existing top-level class agree. Dynamic class expressions remain
unmapped. Source hierarchy anchors remain explicit host declarations.

Structural reparenting keeps its render island by default. An explicit
`boundary: 'destination'` adopts the destination condition and repetition only
when free references retain their lexical declarations; cross-file moves must
carry no external value or type references. Existing item keys are preserved,
and known duplicate destination keys are rejected. This source contract does
not prove layout, type narrowing or React instance-state preservation: the host
must run its build and affected-screen QA before publishing the reviewed plan.

The source service admits only the exact reviewed plan to preview or proposal.
Integrations can configure `proposalValidation` on the source, command, token,
font, and file-publication services. `createSourceProposalValidation` from
`/source-service` issues bounded server receipts only after trusted build and QA
callbacks pass for the exact target, complete file plan, and preview request.
Browser success flags and preview URLs do not issue receipts. Omitting the gate
preserves the diff-review-only integration contract.

`createDraftSourceProposalValidation` owns an isolated preview per validation.
Provide `createPreview`, a trusted `resolvePreview` route resolver, `runQa`, and
`assertTarget`; it builds, runs QA, and disposes the preview before returning.
Its resolver receives the already reviewed request and must preserve it exactly.
It must not reenter the source service while a proposal holds its operation lock.
File-only reviews receive a server-created source-plan preview envelope.

`createDurableSourceProposalPublisher` also supports separate branch and proposal
callbacks. Keep the same storage directory and namespace when upgrading from the
single `publish` callback. Older successful results remain reusable; older
uncertain operations can only recover an existing proposal through `reconcile`.

```ts
const source = { projectRoot, appDirectory: 'app', remote: 'origin', baseBranch: 'main' };
const propose = createDurableSourceProposalPublisher({
  storageRoot, namespace: 'project:source-proposals', source,
  publishBranch: ({ plan, target, publicationId, signal, beforeRemoteWrite }) =>
    publishSourceFilePlan({ ...source, plan, target, proposalValidation, signal,
      branch: `proposals/${publicationId}`, commitMessage: 'Update reviewed source',
      beforeRemoteWrite }),
  async createProposal({ published, details, signal, beforeRemoteWrite }) {
    const prepared = await prepareDraftProposal({ published, details, signal });
    await beforeRemoteWrite();
    return createDraftProposal({ ...prepared, branch: published.branch, signal });
  },
  reconcile: findExistingProposal,
});
```

The host supplies its provider's read-only lookup, credential/body preparation,
and Draft-PR creation. The SDK persists the confirmed branch receipt before
proposal preparation. Immediately before creation, the boundary verifies the
remote destination, branch and base commits, complete changed-file contents and
modes, and all original source proofs. Concurrent resumes cannot create another
proposal. A preparation failure or cancellation can retry that stage without
another push. Fetch and push destination hashes bind the stored review; changing
the remote or using multiple push destinations blocks resumption.

A lost push response before its receipt, a crash while a claim is active, and
an uncertain proposal-creation response remain blocked from further writes.
`reconcile` may recover an existing proposal, including a closed one; an empty
lookup does not prove the earlier write failed. This API never merges a proposal
or updates an existing remote branch. `verifyPublishedSourceFilePlan` is also
available from `/source-files` for read-only verification against the configured
remote, without issuing a new build or QA receipt.

For an input slot, the catalog declares `kind: 'input-prop'` and its `prop`.
The capture declaration identifies that same attribute and its existing expression:

```ts
const caption = { kind: 'member', object: { kind: 'identifier', name: 'record' }, property: 'caption' } as const;
const declaration = {
  file: 'src/View.tsx', attribute: 'data-id', value: 'entry',
  kind: 'input-prop', prop: 'label', slotId: 'label', expected: 'record.title',
  identifiers: ['record'], helpers: ['formatLabel'],
  replacement: { kind: 'literal', value: 'Untitled' },
  fields: [{ fieldId: 'caption', expression: caption, formats: [{
    formatter: { id: 'label', options: { prefix: '#' } },
    expression: { kind: 'call', helper: 'formatLabel', arguments: [caption, { kind: 'literal', value: '#' }] },
  }] }],
} satisfies Parameters<typeof mapCapturedDataTargets>[0]['declarations'][number];
```

Each formatter/options combination requires an exact `formats` declaration;
the helper must already exist in source. Missing values bypass the formatter
and retain the binding's fallback policy. Boolean inputs remain booleans.
Input mappings cannot replace event handlers, key/ref, children, or attributes
affected by a spread. Hiding an entire component requires a visibility mapping.
For a shared row override, a formatter must resolve to the same helper symbol
at the display and collection sites; shadowed helpers remain blocked.

`replacement` declares the expression used when disconnecting a source-owned
binding. A repeated collection can declare its replacement receiver while
retaining the existing callback, stable keys, and item expressions. A text
declaration can additionally set `writtenReplacement: true` to allow **Use written
content**. That command records `writtenReplacements` in the binding document's
single history transaction, so deliberately typing the existing text also works.
Ordinary disconnection and capture never record a sampled value as authored text.
The source plan records disconnect operations separately from connected references.

Unmapped edits remain blocked; a host must not discard them to produce a partial
proposal. A diagnostic's target opens the corresponding layer for correction.

### Paste layers with styles and prototype connections

Workspace copy includes the selected graph, its referenced component definitions
and variant sets, and the styles and prototype declarations that graph uses.
Pasting creates new local identities automatically when all relationships fit.
Name conflicts and connections outside the copied graph open a dialog before
changing the document. Resolve names, choose whether to keep copied flow starts,
and select existing destination screens. Internal IDs do not need manual editing.
An existing style may be reused only through an explicit selection that matches
its complete value and library origin. Target presentation settings are retained.
Copied flow starts can be explicitly omitted; keeping a starting screen below a
nested container is refused.

Programmatic consumers use `copyDesignSubtreesWithRelationships(document,
selection, { flowPolicy: 'selected-starts' | 'none' })`, the matching serialization
and parsing functions, and `pasteDesignSubtreesWithRelationships`. The paste
options require complete `styles` and `prototype` relationship maps for a version
2 envelope. The resulting plan contains every node, asset and owned sidecar;
`applyDesignTransferPaste(transaction, currentDocument, plan)` checks the target
fingerprint before replacing it in one undoable transaction. Changes to the
document, page, source or mode invalidate an open Workspace paste.

Plain content still emits version 1. The original version 1 APIs keep their
existing relationship refusal behavior. Both formats preserve component override
addresses through the existing structural codec. Variable-library bindings,
data/source bindings and unknown document sidecars still require their own
explicit adapters. Clipboard transfer does not write a token library, fetch a
remote asset or apply implementation source.

### Measure an authored plain-text occurrence

`createTextLayoutPlan` captures one visible resolved text occurrence with an
explicit document revision key and instance/row address. `createTextLayoutRuntime`
uses the exact font preparation and registered face resolver to measure that
plan in a browser. It returns immutable local CSS-pixel bounds, line baselines,
UTF-16 grapheme ranges and face/environment identity. Fixed, auto-width and
auto-height modes follow the existing text surface. Native shaping remains on
the complete paragraph; character ranges only observe the resulting geometry.

Each acquisition rechecks face registration, including cache hits. Concurrent
readers share work, cancellation respects remaining readers, and successful
entries use a bounded cache. Dispose the runtime when its font environment ends.
Mixed text, bidi, connected/repeated content, unresolved typography and
source-rendered text return named diagnostics in this first measurement API.

When both font capabilities are supplied, Workspace shares a disposable measured
scene between the canvas, selection handles, snapping, guides, alignment and
prototype wiring. Auto-height text keeps its authored minimum height when its
width changes; auto-width text owns both dimensions. Inspector sizes follow the
measured scene and derived dimensions cannot be overwritten by resize controls.
Pending or unavailable derived bounds stay unavailable to geometry commands.
Changing the document, page, source, mode or font registration cancels stale
geometry work and gestures. Late measurements never reset a camera the user moved.

This integration supports plain text under the existing bounded layout solver.
Non-start constraints under text-resized ancestors, mixed text and dynamic
prototype playback still need additional geometry integration. The built-in
multiline exporter is also separate work. Measurement never rewrites canonical
width/height, adds an undo entry or changes source files.

Variable selection, host structure policy, theming slots, and remaining functional gaps are documented in [Variable binding workflow](./VARIABLES.md).

### Inspect an existing variable binding

`resolveVariableModeForNode` and `resolveVariableModeForInstance` return the
requested collection mode and the scope that supplied it. Instance addresses
use the existing opaque override address; consumers do not reconstruct paths.
`planVariableOccurrence` captures one actual bound occurrence, its token
resolution (including the authored mode), scalar value and literal patch without
refreshing unrelated nodes. `projectVariableScalar` exposes the same validation
and own-node or sparse-override projection used by refresh. These reads do not
write either document or enable variable-library clipboard transfer.

### Choose the language the editor speaks

The editor ships English and Korean. The **Language** control in the workspace
header switches between them, and the Variables dialog carries the same control
so a reader inside it does not have to close it first. The choice is kept per
browser under `pygmalion.language`; without one the editor follows the
browser's own language list and falls back to English.

A host can install another language. It supplies the keys it cares about, and
every key it leaves out stays English, so a partial translation never blanks a
control:

```ts
import { registerEditorLanguage, setEditorLanguage } from '@pygmalionjs/pygmalion';

registerEditorLanguage({
  id: 'fr',
  label: 'Français',
  messages: { 'variables.title': 'Variables', 'variables.review': 'Revoir les modifications' },
});
setEditorLanguage('fr');
```

`editorMessages()` reads the active language outside React and `useMessages()`
inside it, both returning a translator that fills the `{name}` values a message
declares. Every editor surface reads from the catalogue: the workspace header,
the storyboard view, the design panels, the code review, the prototype player,
the canvas inspect overlay and the shell the editor falls back to. A host's own
words — screen names, flows, field labels, presets — stay as the host wrote
them.

### Recover token applications after a reload

`createTokenSourceService` keeps Git archives immutable. To continue editing
local files after Apply, opt in with `workingCopy: { stateDirectory }`. The
state directory must be outside the application directory and shared by SDK
processes serving that project. Core stores reviewed requests there for up to
24 hours, with defaults of 64 records and 128 MiB per project. Active writers
cannot be evicted. These limits are configurable through `workingCopy`.

Connect the optional receipt through the existing host transport:

```ts
const tokenLibrary = {
  // Existing load, draft, codec, compile and preview declarations remain.
  applySource(request, { signal, receipt }) {
    return rpc('apply', receipt ? { request, receiptId: receipt.id } : request, signal);
  },
  recoverSourceApply(receipt, { signal }) {
    return rpc('recoverApply', receipt, signal);
  },
};
```

Add `recoverApply` to the transport's operation allowlist. With recovery enabled,
Apply requires the exact receipt returned by the completed review. A lost HTTP
response can be confirmed after a server restart only when the complete file
manifest, consumer inventory, source revision and codec result still match.
An active writer returns `pending`; partial writes and external changes cannot
become a successful baseline. The UI distinguishes matching project files from
confirmed regeneration.

`load` still returns the committed archive. Recovery returns a separate
`baseline.basis.kind === 'working'` with actual file proofs. The next review uses
that working basis in an isolated archive, so 6→7 followed by 7→8 works without
moving HEAD. Built previews use the existing `prepareSource` callback:
`({ request, appRoot, signal }) => tokenService.preparePreviewSource(request, appRoot, signal)`.
Keep this filesystem callback server-side. The build callback receives the
validated `documentRevision`; pass that value to the built app instead of
recalculating a hash in the host. Working preview revisions hash the
complete request identity, including its source basis. Archive preview revisions
retain their existing document hash.

A pull request uses a separate, explicit review from the committed source: after
local 6→7, proposing 8 reviews the cumulative 6→8 diff. Expired or conflicting
local evidence can be discarded with **Reopen committed source**. This reopens
the archive for review; it does not import or overwrite arbitrary working edits.

Recovery confirms present bytes. A file changed and then restored while the
server was stopped is indistinguishable from an untouched matching file. A crash
with all original bytes present cannot prove whether the writer never ran or
rolled back. Neither case manufactures a successful local application.

Draft saving is separate: one controller serializes `saveDraft` calls, and an
exit checkpoint preserves its latest edits. The API cannot cancel or conditionally
order a remote save from an earlier page. A late remote write can still arrive
after the next page has loaded; hosts needing that guarantee must provide
versioned persistence. Synchronous local storage does not have that race.

## Local development

```bash
npm install
npm run dev
```

Validation commands:

```bash
npm test
npm run typecheck
npm run build:lib
npm run lint:language
```

The optional native input check builds the public entry and runs seventeen Chromium
scenarios through the complete editor. Install Chromium once, then run:

```bash
npm exec -- playwright install chromium
npm run test:browser:input
```

On Linux, `npm exec -- playwright install --with-deps chromium` also installs
browser system dependencies. Shortcuts use `ControlOrMeta`: Meta on macOS and
Control on Linux. The fixture grants clipboard permissions only to its local origin.

The seventeen scenarios cover:

- Layer focus after deleting the last branch, and Tab entry into an empty tree
  followed by native Copy/Paste, row navigation, and Undo/Redo.
- The same layer flows inside an open shadow root, including clicking the
  already focused last row again before Delete and Undo/Redo.
- Variable selection while scrolling beneath a stationary pointer, pointer
  movement followed by Enter, mouse click, and touch tap. Each checks Undo/Redo.

- Vector input keys in the document and an open shadow root preserve selected
  anchors and pen drafts. Real canvas gestures still support Escape cancellation,
  Enter or Delete, and Undo/Redo.
- Two independent Run sessions in the document and open shadow roots keep the
  first iframe, input value, viewport, and workspace state when Escape returns
  the second editor from its toolbar to Edit.
- Prototype player controls in the document and an open shadow root cycle with
  Tab and Shift+Tab, activate Pause/Resume, Restart, and Close with Enter, and
  return focus to the opener without changing the edited document.

Missing Chromium, unavailable clipboard access, missing native event evidence,
and failed assertions return a nonzero exit code. Each run saves JSON results and
available screenshots in a unique temporary directory printed by the command;
pass an artifact parent directory after `--` to choose its location. Reusing that
parent preserves prior runs. This command is separate from `npm test`,
`prepublishOnly`, and the latency suite.

To run these checks in GitHub Actions, open the **CI** workflow, choose **Run
workflow**, and enable `native_editor_input`. The optional job uses Ubuntu 24.04
and Node 24, installs Chromium with its system dependencies, and has a 15-minute
limit. It uploads JSON results and available PNG screenshots after success or
failure; missing artifacts fail the upload step. Push and pull request runs do
not start this browser job.

## Project documentation

- [Architecture](./ARCHITECTURE.md) defines the package and host boundary.
- [Design compiler and component connections](./docs/design-compiler.md) defines the versioned IR, target adapters, diagnostics, and external design mappings.
- [Screen state contract](./docs/screen-state-contract.md) defines frame versus same-frame placement and held pseudo-state capture.
- [Editor usability](./EDITOR-USABILITY.md) defines interaction coverage.
- [Product readiness](./PRODUCTIZATION.md) defines quality gates and remaining
  work.
- [The editor transport](./docs/transport.md) defines the one object every
  request goes through, its endpoint table, and its error model.
- [Coverage contracts](./docs/coverage-contracts.md) defines how a host's
  source-route gate identifies routes, represents them by preview, mapping,
  or fixture, classifies scenarios exactly once, and joins diagnostics back
  to source.
- [Upgrading to 0.7](./docs/migration-0.7.md) maps the removed module-level
  exports to the editor runtime and the removed endpoint props to the
  transport.
- [Upgrading to 0.8](./docs/migration-0.8.md) lists what moved onto the
  runtime, the coverage contracts, the live canvas, and a 0.6 to 0.8
  checklist for a Next.js host.
- [Contributing](./CONTRIBUTING.md) defines the required commit format and the
  release flow.
- [Changelog](./CHANGELOG.md) lists what each published version changed.

## License

Pygmalion is released under the [Apache License 2.0](./LICENSE).


### Connect an authored document to a React source file

For applications that consume a complete `DesignDocument` JSON file, declare
`WorkspaceSourceCapabilities.documentFile` with its proven repository-relative
path and supply `compileSource` using `compileWorkspaceDocumentFile`. Keep the
React entry point and JSON file in the baseline's complete file-proof list.
Use the Node `createWorkspaceSourcePlanService` with `previewSourceFilePlan` and
`applySourceFilePlan` to recheck server-owned bytes and require an exact review
before writing. Preview returns the complete file diff, including long documents.

This opt-in stores every layer, path, instance override and metadata field in the
JSON source; the editor rejects a compiler that drops any of them. Existing
JSX/style adapters keep their mapped-field and unsupported-feature diagnostics.
Saving a browser draft remains separate from source apply.

```tsx
import { DesignDocumentView, type DesignDocument } from '@pygmalionjs/pygmalion';
import design from './Screen.design.json';

export function Screen() {
  return <DesignDocumentView document={design as DesignDocument} rootId="screen" />;
}
```

`DesignDocumentView` renders a top-level frame using the workspace's layout,
component instances, text and vector surfaces, without editing interactions or
hover outlines. It is a static React view, not a standalone JSX/CSS exporter or
a prototype player. Connected data and external components still need their
explicit `projection` and `configuration` providers. Hosts own source paths,
initial files, API routing and fonts; core owns serialization and review policy.


The Code inspector displays selected-layer CSS immediately, including when the
draft matches source or has no writeback provider. Vector layers also expose SVG;
Document JSON contains the complete current draft. These generated views are
labeled separately from actual files. Hosts can supply `readSourceFiles(target,
{ signal })` with `{ file, content, sha256 }` records to enable the connected-source
viewer. The viewer verifies complete bytes, cancels stale reads, supports refresh
and copy, and reloads after source apply. File inspection grants no write permission
and does not require reviewing a change first.

Code views use a read-only CodeMirror editor with language highlighting, line
numbers, search, folding, wrapping, file tabs, and an expanded dialog. The editor
loads on demand, uses the existing UI theme, and keeps keyboard input separate
from canvas commands. Top-level screen placement arranges the canvas; its
translation and rotation are excluded from generated CSS just as the static
screen view starts at the origin. Nested layer geometry remains inspectable.
Canvas placement still belongs to the complete saved design document.

Canvas gestures preserve the distinction between size rules and placement rules.
An auto-layout child can be dragged to a new insertion boundary without disabling
Hug or Fill sizing. The canvas previews its outline and destination before a
single undoable commit; Escape, lost pointer capture, or stale geometry cancels
it. A child can also leave its flow for the canvas or a free-layout frame,
retaining its displayed size and final world position even when removing it
shrinks a Hug ancestor. Component-definition ownership remains intact. Drops
into another auto-layout container are currently refused with feedback. A single
free-positioned child can align to its parent's bounds.

Rotation starts just outside a selection corner. It uses the pointer's actual
start position, keeps the center fixed, and snaps to 15-degree steps with Shift.
A single layer's size badge retains its intrinsic width and height when rotated.
For independently editable fixed dimensions, the persistent aspect-ratio lock
links numeric width/height edits and canvas resizing. Min/max bounds clamp both
axes by one common scale. Ctrl temporarily suspends
the stored ratio lock and object snapping; explicit guides and grids still snap.
Rulers remain available as canvas measurement tools. Custom rotation pivots are
not implemented.

## Proposal and history QA

`captureDraftProposalQa` from `@pygmalionjs/pygmalion/qa` runs declared
component, viewport and color-scheme variants in separate browser contexts.
Each built page must report the exact `documentRevision` through the draft-ready
protocol after its product recipe completes. Declared follow-up interactions and
assertions then run before evidence collection; the accessibility audit checks
the final asserted state.
The caller owns the browser; the SDK closes every capture context, including on
failure and cancellation.

```js
const report = await captureDraftProposalQa({
  browser, surface, identity,
  variants: [
    { id: 'card:compact:light', componentId: 'card', width: 480, height: 800, theme: 'light' },
    { id: 'card:wide:dark', componentId: 'card', width: 1280, height: 800, theme: 'dark' },
  ],
  policy: {
    defaults: { required: ['recipe', 'viewport', 'theme', 'accessibility'] },
    components: { card: { maxAccessibilityViolations: 0 } },
  },
  signal,
});
```

DOM and pixel comparison require baseline hashes and PNGs for each variant;
missing evidence stays blocked when the policy requires it. Component rules can
set pixel thresholds, allow structural differences, and require an explicit
reviewer approval bound to both `policyHash` and `evidenceHash`. Approval never
waives a failed check. The default accessibility audit checks visible control
names, image alternatives and document language; provide `auditAccessibility`
for a broader audit. Theme evidence measures the browser color scheme, not the
product's token or contrast correctness. `onCapture` receives PNG and heatmap
bytes for persistence; the returned report contains JSON metadata and hashes.

Design sessions persist undo, redo and arbitrary recorded revision restoration
as new audited commits. They require the current source, revision and HEAD;
a divergent or dirty worktree is rejected. `SessionHistoryControl` consumes
`usePygmalionProject` and exposes the source history without reusing document
undo. Configure `sessions.historyQa` to enforce affected-screen QA before and
after a history mutation. The callback receives exact source/HEAD identities,
changed files, the session app root and the previous phase's result. If either
phase fails or is cancelled, the SDK restores source files, HEAD and the session
record. The host declares which screens those files affect.

`createWorkspaceRevisionPreviewService` from `/draft-preview` builds an exact
committed revision for this callback without inventing a source edit. Its
`prepare({ sourceRevision, screenId, route, context })` method supplies the
host's JSON recipe context to the trusted build callback, reusing isolated
archives, bounded static output, separate origins and disposal. Working-copy
staging is prohibited. The browser-facing draft service continues to reject
revision-only requests; this helper does not issue proposal receipts.

With `qa.storage` configured, `createPygmalionVitePlugins` can resolve persistence
identity from a current design-session preview. It rechecks the trusted identity
after capture and rejects revision drift. The project runtime asks the source
preview service to start a session baseline when visual QA is enabled. A shared
mirror URL cannot establish a session identity; standalone mirror captures need
an explicit trusted `resolveIdentity` callback.

## Draft previews in a remote editor

The default draft builder returns an ephemeral loopback URL for local use. When
an editor runs on another machine, configure dedicated browser origins and
reverse proxy each complete origin to a fixed loopback port:

```ts
import { createWorkspaceDraftPreviewService } from '@pygmalionjs/pygmalion/draft-preview';

const service = createWorkspaceDraftPreviewService({
  projectRoot,
  build: buildDraft,
  handleRequest: handlePreviewRequest,
  remoteOrigins: [
    { port: 5191, origin: 'https://preview-one.example.test' },
    { port: 5192, origin: 'https://preview-two.example.test' },
  ],
});
```

Each active build or preview reserves one origin until its request is cancelled
or its lease is released. Exhaustion fails immediately so the caller can close
an existing preview and retry. A failed build frees its slot. The default active
run limit is three, capped by the number of configured origins.

The proxy must preserve the preview origin's Host or rewrite it to the exact
`127.0.0.1:<port>` authority. Forward the full origin, including root-relative
assets and host-mocked API routes. A path beneath the editor origin cannot
provide this isolation. Do not forward editor credentials, set editor cookies,
or expose editor API routes on a preview origin. Prefer separate hostnames:
cookies are scoped by hostname, not port. HTTPS editors require HTTPS previews.
Service worker registration is blocked so a worker cannot outlive its lease and
intercept the next artifact on a reused origin.

The preview client rejects editor-origin URLs, mixed content, and server-loopback
URLs returned to a remote browser before mounting an iframe. Configuring a public
origin does not publish a server or provide authentication; the host owns the
reverse proxy, TLS, and access policy. Source identity validation, isolated
builds, origin lifetimes, and cancellation remain SDK responsibilities.

## Reuse a selected frame for witnessed state transitions

A replay axis can declare `liveTransitions` to update the selected storyboard
frame without replaying its entry sequence or requesting a new server capture:

```ts
const open = [{ action: 'click', label: 'Open menu', selector: '#menu-trigger' }] as const;
const closed = [{ label: 'Menu closed', selector: '#menu', exists: false }];
const visible = [{ label: 'Menu visible', selector: '#menu', exists: true }];

const menu: ScreenStateAxisDef = {
  id: 'menu', label: 'Menu', kind: 'interaction',
  options: [
    { id: 'closed', label: 'Closed' },
    { id: 'open', label: 'Open', steps: open },
  ],
  liveTransitions: [
    { from: 'closed', to: 'open', steps: open, before: closed, after: visible },
    {
      from: 'open', to: 'closed', before: visible, after: closed,
      steps: [{ action: 'press', label: 'Dismiss menu', selector: '#menu', key: 'Escape' }],
    },
  ],
};
```

Declare both directions using the application's existing interactions. The SDK
does not infer how to undo a click. Before and after assertions are mandatory;
entry replay also requires baseline assertions. The selected document boots once,
then validates each declared edge and the baseline assertions. While a change is
pending, the last verified inert snapshot remains visible. Only the verified
current document is revealed; a live snapshot does not certify an exact capture.
The inspector and 100% preview use the same verified snapshot. Opening the
larger preview does not boot another document or request a server capture;
pending transitions retain the previous verified image with an updating status.

This path supports one single-choice replay axis at a time. Combined axes,
direct-state combinations, boot conditions, held pseudo states, undeclared
recipes, or missing edges use the existing captured path. Revision epochs,
environment changes, and replacement preparation controllers discard the old
boot. A missing witness, navigation, or a 15-second deadline invalidates the live
document and resumes capture. Aborted and superseded work cannot reveal a result.
No host component hook is needed.

Browser replay and Node capture share a target-driven settle policy. An action
followed by a targeted wait avoids an extra fixed pause; explicit wait settling
and timer-only waits remain intact. `captureStoryboardCase` uses this policy by
default; pass `interactionRunOptions: { settlePolicy: 'authored' }` to preserve all
authored pauses. The standalone `runStoryboardInteraction` API keeps its authored
default. Existing axes without `liveTransitions` retain their capture behavior.
