diff --git a/README.md b/README.md index 66823d6..6be0c4a 100644 --- a/README.md +++ b/README.md @@ -49,23 +49,27 @@ For the maintainer workbench: pnpm dev:all ``` -Open `http://localhost:5173/generate.html`. +Open `http://localhost:5173/generate`. 1. Choose the **Host Data Search** showcase scenario. -2. Confirm the run is interactive and only the `search` host tool is allowed. +2. Confirm the agent broker selects an interactive run with only the `search` + host tool allowed. 3. Run it, then submit a generated search such as `chicken pasta`. -4. Open `http://localhost:5173/adversarial.html` and confirm the sandbox +4. Open `http://localhost:5173/adversarial` and confirm the sandbox boundary still holds. To steer generation from a Ghost fingerprint, set `SUMMON_GHOST_ROOTS` in `apps/server/.env` before starting the demos. Each configured root should use -the canonical `.ghost/fingerprint/manifest.yml` package layout; legacy -`.ghost/fingerprint.yml` roots are bridged for compatibility. The Surface -Gallery adds a Ghost preset for each root, and the Generate workbench adds both -the Ghost scenario and the `Ghost · ` direction option. +the canonical `.ghost/fingerprint/manifest.yml` package layout. The Surface +Gallery adds a Ghost fingerprint preset for each root, and the Generate +workbench adds a `Fingerprint · ` option. A fingerprint run is not a bundled +visual direction: Summon consumes the Ghost relay brief as product design +direction, then applies host-owned policy, capabilities, and token CSS. The full guided path lives in [docs/adoption/quickstart.md](docs/adoption/quickstart.md). +The architecture boundary is documented in +[docs/ghost-fingerprint-architecture.md](docs/ghost-fingerprint-architecture.md). ## How It Fits Together @@ -86,16 +90,17 @@ registered host tools. - `examples/surface-gallery` - primary adopter gallery with curated live presets, compact host tools, Ghost-root presets when configured, a sandboxed surface, and a small event strip. -- `/generate.html` - diagnostic maintainer workbench for surface configs, allowed host - tools, trusted host components, token overrides, validation retries, - edit/replay, Ghost steering, Devtools, and stream diagnostics. -- `/batch.html` - parallel prompt harness for prompt coverage, host tool wiring, - direction-token visual coverage, throughput, and consistency checks. -- `/adversarial.html` - sandbox boundary checks for network, storage, parent +- `/generate` - diagnostic maintainer workbench for broker-selected + surface configs, allowed host tools, trusted host components, token + overrides, validation retries, edit/replay, Ghost steering, Devtools, and + stream diagnostics. +- `/batch` - parallel broker harness for prompt coverage, host tool + wiring, direction-token visual coverage, throughput, and consistency checks. +- `/adversarial` - sandbox boundary checks for network, storage, parent access, and unallowed host tool requests. -- `/strict.html` - trusted host overlay for sensitive input inside a generated +- `/strict` - trusted host overlay for sensitive input inside a generated sandbox description. -- `/fatal.html` - sandbox startup failure handling. +- `/fatal` - sandbox startup failure handling. ## Public Packages diff --git a/apps/demo/adversarial.html b/apps/demo/adversarial.html deleted file mode 100644 index da8b4f9..0000000 --- a/apps/demo/adversarial.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - -Summon — adversarial harness - - - - - -

Phase 1 adversarial harness

-

Loads a sandbox with a deliberately malicious artifact. Each attempt that fails is a win.

- -
-
-
Sandbox iframe
- -
-
-
Test results
-
-
Running…
-
-
- - - - diff --git a/apps/demo/batch.html b/apps/demo/batch.html deleted file mode 100644 index 32a417a..0000000 --- a/apps/demo/batch.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - -Summon — batch testing - - - - - -

Batch testing

-

Fire N generations in parallel. Same prompt to compare consistency, or a seeded random sample from the curated prompt pool to compare coverage.

- -
- - -
- - -
- -
- - -
- -
- - -
- - - - - - - -
- -
-
No run yet.
- - - - diff --git a/apps/demo/fatal.html b/apps/demo/fatal.html deleted file mode 100644 index 45c7932..0000000 --- a/apps/demo/fatal.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - -Summon — sandbox self-test - - - - - -

Bootstrap self-test

-

Drives bootstrap into a deliberately misconfigured sandbox. Bootstrap's startup self-test should detect the regression and post SUMMON_FATAL instead of SUMMON_READY.

- -
-

Case A — correctly configured (control)

-

sandbox="allow-scripts" — null-origin. Self-test should pass and bootstrap should post SUMMON_READY.

-
-
-
Sandbox iframe
- -
-
-
Result
-
-
-
-
- -
-

Case B — misconfigured (allow-same-origin)

-

sandbox="allow-scripts allow-same-origin" — same-origin with parent. Self-test should detect and post SUMMON_FATAL.

-
-
-
Sandbox iframe
- -
-
-
Result
-
-
-
-
- - - - diff --git a/apps/demo/generate.html b/apps/demo/generate.html deleted file mode 100644 index 6f2a675..0000000 --- a/apps/demo/generate.html +++ /dev/null @@ -1,228 +0,0 @@ - - - - - -Summon — Generate - - - - - -
-

Generate

-

Scenario-led generative UI workbench

-
- -
- - -
-
-
-
Showcase
-

Host Data Search

-

Host-owned data with explicit read authority.

-
-
- pending - 0 host tools -
-
- -
- -
- - -
-
- - - - - -
-
- Sandbox - idle -
-
- -
-
Host Data Search awaits generated UI.
-
-
-
- -
-
- - -
- -
-
- - - - -
- -
-
-
- - - - - - -
- - - - diff --git a/apps/demo/index.html b/apps/demo/index.html index 38ff7f9..327a0a8 100644 --- a/apps/demo/index.html +++ b/apps/demo/index.html @@ -4,55 +4,24 @@ Summon - + - -
- -
+ +
+ diff --git a/apps/demo/package.json b/apps/demo/package.json index 626e550..95f6f45 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -4,19 +4,30 @@ "private": true, "type": "module", "scripts": { - "dev": "pnpm --filter @summon-internal/devtools --filter @summon-internal/engine --filter @summon-internal/host --filter @summon-internal/sandbox-runtime build && pnpm --filter @anarchitecture/summon build && vite", - "build": "pnpm --filter @summon-internal/devtools --filter @summon-internal/engine --filter @summon-internal/host --filter @summon-internal/sandbox-runtime build && pnpm --filter @anarchitecture/summon build && vite build", + "dev": "pnpm --filter @summon-internal/devtools --filter @summon-internal/engine --filter @summon-internal/host --filter @summon-internal/sandbox-runtime build && pnpm --filter @anarchitecture/summon build && pnpm --filter @summon-internal/react build && pnpm --filter @anarchitecture/summon-react build && vite", + "build": "pnpm --filter @summon-internal/devtools --filter @summon-internal/engine --filter @summon-internal/host --filter @summon-internal/sandbox-runtime build && pnpm --filter @anarchitecture/summon build && pnpm --filter @summon-internal/react build && pnpm --filter @anarchitecture/summon-react build && vite build", "preview": "vite preview", "test": "tsx --test src/*.test.ts", "typecheck": "tsc --noEmit" }, "dependencies": { "@anarchitecture/summon": "workspace:*", + "@anarchitecture/summon-react": "workspace:*", "@summon-internal/engine": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.17.0", "zod": "^3.23.0" }, "devDependencies": { + "@tailwindcss/vite": "^4.3.0", + "@types/react": "^18.3.31", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^6.0.2", + "autoprefixer": "^10.5.0", + "postcss": "^8.5.15", + "tailwindcss": "^4.3.0", "typescript": "^5.4.0", - "vite": "^5.4.0" + "vite": "^8.0.16" } } diff --git a/apps/demo/public/summon.css b/apps/demo/public/summon.css deleted file mode 100644 index eb6ffbb..0000000 --- a/apps/demo/public/summon.css +++ /dev/null @@ -1,2094 +0,0 @@ -/* Summon demo styling — Ghost-compatible monochrome demo shell using local - system font stacks and no bundled remote font assets. */ - -:root { - color-scheme: light; - - /* Monochromatic gray scale */ - --color-white: #ffffff; - --color-black: #000000; - --color-gray-50: #f5f5f5; - --color-gray-100: #f0f0f0; - --color-gray-200: #e8e8e8; - --color-gray-300: #e5e5e5; - --color-gray-400: #cccccc; - --color-gray-500: #999999; - --color-gray-600: #666666; - --color-gray-700: #333333; - --color-gray-800: #232323; - --color-gray-900: #1a1a1a; - - /* Utility palette */ - --color-red-200: #f94b4b; - --color-blue-200: #5c98f9; - --color-green-200: #91cb80; - --color-yellow-200: #fbcd44; - - /* Semantic backgrounds */ - --background-default: var(--color-white); - --background-alt: var(--color-gray-50); - --background-muted: var(--color-gray-100); - --background-inverse: var(--color-black); - --background-accent: var(--color-gray-900); - - /* Semantic borders */ - --border-default: var(--color-gray-200); - --border-input: var(--color-gray-300); - --border-input-hover: var(--color-gray-400); - --border-strong: var(--color-gray-900); - - /* Semantic text */ - --text-default: var(--color-gray-900); - --text-alt: var(--color-gray-600); - --text-muted: var(--color-gray-500); - --text-inverse: var(--color-white); - --text-danger: var(--color-red-200); - --text-success: var(--color-green-200); - --text-info: var(--color-blue-200); - --text-warning: var(--color-yellow-200); - - /* Shape — pill-first radius system */ - --radius-pill: 999px; - --radius-card: 20px; - --radius-card-sm: 14px; - - /* Shadows — 4-tier hierarchy */ - --shadow-card: 0 2px 8px rgba(76, 76, 76, 0.15); - --shadow-btn: 0 2px 8px rgba(76, 76, 76, 0.15); - --shadow-elevated: 0 3px 12px rgba(76, 76, 76, 0.22); - - /* Heading scale — tight tracking with strong weight for demo headers. */ - --heading-page-size: clamp(28px, 3.2vw, 40px); - --heading-page-line-height: 1; - --heading-page-letter-spacing: -0.025em; - --heading-page-weight: 700; - - --heading-card-size: clamp(18px, 1.6vw, 22px); - --heading-card-line-height: 1.15; - --heading-card-letter-spacing: -0.01em; - --heading-card-weight: 600; - - /* Label — uppercase kicker (used for pane headers) */ - --label-size: 11px; - --label-letter-spacing: 0.12em; - --label-weight: 600; - --label-line-height: 1.2; - - /* Fonts */ - --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; - - /* Timing */ - --duration-fast: 0.15s; - --duration-normal: 0.2s; -} - -* { - box-sizing: border-box; -} - -html { - -webkit-text-size-adjust: 100%; - scroll-behavior: smooth; -} - -body { - margin: 0; - padding: 40px 32px 80px; - max-width: 1200px; - background: var(--background-default); - color: var(--text-default); - font-family: var(--font-sans); - font-feature-settings: "rlig" 1, "calt" 1; - font-size: 14px; - line-height: 1.5; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body.wide { - max-width: none; -} - -::selection { - background: var(--color-gray-900); - color: var(--color-white); -} - -/* Scrollbars */ -::-webkit-scrollbar { width: 6px; height: 6px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--color-gray-400); border-radius: 999px; } -::-webkit-scrollbar-thumb:hover { background: var(--color-gray-500); } - -/* ─── Page header ─── */ - -h1.page-title { - margin: 0 0 6px; - font-size: var(--heading-page-size); - line-height: var(--heading-page-line-height); - letter-spacing: var(--heading-page-letter-spacing); - font-weight: var(--heading-page-weight); - color: var(--text-default); -} - -p.lede { - margin: 0 0 32px; - color: var(--text-alt); - font-size: 15px; - line-height: 1.55; - max-width: 72ch; -} - -p.lede code { - font-family: var(--font-mono); - font-size: 0.92em; - padding: 1px 6px; - background: var(--background-muted); - border-radius: 6px; -} - -/* ─── Nav (pill buttons row) ─── */ - -nav.summon-nav { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 32px; -} - -nav.summon-nav a { - display: inline-flex; - align-items: center; - height: 32px; - padding: 0 14px; - font-size: 13px; - font-weight: 500; - color: var(--text-alt); - text-decoration: none; - background: transparent; - border: 1px solid transparent; - border-radius: var(--radius-pill); - transition: - color var(--duration-fast) ease, - background-color var(--duration-fast) ease, - border-color var(--duration-fast) ease; -} - -nav.summon-nav a:hover { - color: var(--text-default); - background: var(--background-muted); -} - -nav.summon-nav a.active { - color: var(--text-inverse); - background: var(--background-accent); - border-color: var(--background-accent); - font-weight: 600; -} - -/* Wordmark — sits at the start of the nav, links back to landing. - Reads as a logo, not a tab. */ -nav.summon-nav a.summon-brand { - height: 32px; - padding: 0 14px 0 2px; - margin-right: 6px; - font-size: 15px; - font-weight: 700; - letter-spacing: -0.02em; - color: var(--text-default); - background: transparent; - border: 0; - border-right: 1px solid var(--border-default); - border-radius: 0; -} -nav.summon-nav a.summon-brand:hover { - background: transparent; - color: var(--text-default); - opacity: 0.6; -} - -/* ─── Pane (card) ─── */ - -.layout { - display: grid; - gap: 20px; -} - -.layout.cols-2 { grid-template-columns: 1fr 1fr; } -.layout.cols-3-2 { grid-template-columns: 1.4fr 1fr; } -.layout.cols-11-1 { grid-template-columns: 1.1fr 1fr; } - -.pane { - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - overflow: hidden; - box-shadow: var(--shadow-card); - display: flex; - flex-direction: column; -} - -.pane > header { - padding: 14px 18px; - background: var(--background-default); - border-bottom: 1px solid var(--border-default); - font-size: var(--label-size); - letter-spacing: var(--label-letter-spacing); - font-weight: var(--label-weight); - line-height: var(--label-line-height); - text-transform: uppercase; - color: var(--text-alt); - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; -} - -.pane > header .status { - font-family: var(--font-mono); - font-size: 11px; - font-weight: 500; - letter-spacing: 0; - text-transform: none; - color: var(--text-muted); -} - -.pane iframe { - width: 100%; - border: 0; - display: block; - background: var(--background-default); -} - -.pane iframe.h-200 { height: 200px; } -.pane iframe.h-320 { height: 320px; } -.pane iframe.h-440 { height: 440px; } -.pane iframe.h-540 { height: 540px; } -.pane iframe.h-640 { height: 640px; } - -/* ─── Log (mono stream) ─── */ - -.log { - padding: 14px 18px; - font-family: var(--font-mono); - font-size: 12px; - line-height: 1.7; - color: var(--text-alt); - overflow: auto; -} - -.log.h-320 { max-height: 320px; } -.log.h-440 { max-height: 440px; } -.log.h-540 { max-height: 540px; } - -.log .pass, .log .op-add { color: #2c8a5a; } -.log .fail, .log .op-error { color: var(--color-red-200); font-weight: 600; } -.log .info { color: var(--text-alt); } -.log .op-set { color: var(--color-blue-200); } -.log .op-meta { color: var(--text-muted); } -.log .raw { color: var(--color-gray-400); } - -.approval-stack { - position: fixed; - right: 20px; - bottom: 20px; - z-index: 80; - display: grid; - gap: 10px; - width: min(360px, calc(100vw - 32px)); -} - -.approval-card { - display: grid; - gap: 8px; - border: 1px solid var(--border-strong); - border-radius: 8px; - background: var(--background-default); - box-shadow: var(--shadow-elevated); - padding: 14px; -} - -.approval-card span { - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 10px; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.approval-card strong { - color: var(--text-default); - font-size: 15px; - line-height: 1.25; -} - -.approval-card p { - margin: 0; - color: var(--text-alt); - font-size: 12px; -} - -.approval-card pre { - max-height: 120px; - margin: 0; - overflow: auto; - border: 1px solid var(--border-default); - border-radius: 6px; - background: var(--background-alt); - color: var(--text-alt); - font-family: var(--font-mono); - font-size: 11px; - line-height: 1.45; - padding: 8px; - white-space: pre-wrap; -} - -.approval-actions { - display: flex; - justify-content: flex-end; - gap: 8px; -} - -.approval-actions button { - min-width: 76px; - height: 32px; - border: 1px solid var(--border-strong); - border-radius: 6px; - background: var(--background-default); - color: var(--text-default); - cursor: pointer; - font-weight: 700; -} - -.approval-actions .approval-approve { - background: var(--background-inverse); - color: var(--text-inverse); -} - -.summary { - padding: 12px 18px; - border-top: 1px solid var(--border-default); - font-size: 13px; - background: var(--background-alt); - color: var(--text-alt); -} - -/* ─── Forms ─── */ - -form.summon-form { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-bottom: 16px; -} - -textarea, input[type="text"], input[type="number"], select { - font-family: inherit; - font-size: 14px; - color: var(--text-default); - background: var(--background-default); - border: 1px solid var(--border-input); - border-radius: var(--radius-card-sm); - padding: 12px 14px; - transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease; -} - -textarea { - flex: 1; - min-height: 60px; - resize: vertical; -} - -textarea:focus, -input:focus, -select:focus { - outline: none; - border-color: var(--border-strong); - box-shadow: 0 0 0 3px rgba(26, 26, 26, 0.08); -} - -select { - cursor: pointer; - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: right 12px center; - padding-right: 32px; - appearance: none; - -webkit-appearance: none; -} - -select.tall { - height: 60px; - border-radius: var(--radius-card-sm); -} - -/* ─── Buttons ─── */ - -button.btn, -button[type="submit"]:not(.btn-pill):not(.btn-secondary):not(.btn-chip) { - display: inline-flex; - align-items: center; - justify-content: center; - height: 44px; - padding: 0 24px; - background: var(--background-accent); - color: var(--text-inverse); - border: 0; - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 14px; - font-weight: 500; - letter-spacing: -0.005em; - cursor: pointer; - white-space: nowrap; - box-shadow: var(--shadow-btn); - transition: - background-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - transform var(--duration-fast) ease, - opacity var(--duration-fast) ease; -} - -button.btn:hover:not(:disabled), -button[type="submit"]:hover:not(:disabled):not(.btn-secondary):not(.btn-chip) { - background: var(--color-gray-700); - box-shadow: var(--shadow-elevated); -} - -button.btn:active:not(:disabled), -button[type="submit"]:active:not(:disabled) { - transform: translateY(1px); -} - -button:disabled { - opacity: 0.4; - cursor: not-allowed; - box-shadow: none; -} - -button.btn-secondary { - display: inline-flex; - align-items: center; - justify-content: center; - height: 44px; - padding: 0 22px; - background: var(--background-default); - color: var(--text-default); - border: 1px solid var(--border-input); - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: - background-color var(--duration-fast) ease, - border-color var(--duration-fast) ease; -} - -button.btn-secondary:hover:not(:disabled) { - background: var(--background-muted); - border-color: var(--border-input-hover); -} - -button.btn-sm { - height: 36px; - padding: 0 18px; - font-size: 13px; -} - -/* Chip — small pill button used for example prompts */ -button.btn-chip { - display: inline-flex; - align-items: center; - height: 28px; - padding: 0 12px; - background: var(--background-muted); - color: var(--text-default); - border: 1px solid transparent; - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: - background-color var(--duration-fast) ease, - border-color var(--duration-fast) ease; -} - -button.btn-chip:hover { - background: var(--background-default); - border-color: var(--border-input-hover); -} - -/* Action variant — sits next to sample chips, reads as a control, not an example. */ -button.btn-chip.btn-chip-action { - background: var(--background-default); - border-color: var(--border-input); - color: var(--text-alt); -} - -button.btn-chip.btn-chip-action:hover:not(:disabled) { - background: var(--background-muted); - border-color: var(--border-strong); - color: var(--text-default); -} - -button.btn-chip:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* ─── Examples row ─── */ - -.examples { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - margin-bottom: 24px; - font-size: 12px; - color: var(--text-muted); -} - -.examples > span { - font-size: 11px; - letter-spacing: var(--label-letter-spacing); - font-weight: var(--label-weight); - text-transform: uppercase; - color: var(--text-muted); - margin-right: 4px; -} - -.examples > span:not(:first-child) { - margin-left: 12px; -} - -/* ─── Controls bar (batch) ─── */ - -.controls { - display: flex; - flex-wrap: wrap; - gap: 12px; - align-items: center; - padding: 16px; - background: var(--background-alt); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - margin-bottom: 20px; -} - -.controls label { - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - color: var(--text-alt); -} - -.controls select, -.controls input[type="text"], -.controls input[type="number"] { - padding: 8px 12px; - font-size: 13px; - background: var(--background-default); - border-radius: var(--radius-pill); -} - -.controls input[type="number"] { width: 64px; text-align: center; } -.controls input[type="number"].seed { width: 90px; } - -.controls textarea { - flex: 1 1 300px; - min-width: 280px; - min-height: 40px; - max-height: 140px; - padding: 10px 14px; - font: 13px var(--font-sans); - resize: vertical; -} - -/* Segmented control — pill group */ -.mode-group { - display: inline-flex; - gap: 2px; - padding: 3px; - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-pill); -} - -.mode-group label { - padding: 4px 12px; - cursor: pointer; - border-radius: var(--radius-pill); - font-size: 12px; - font-weight: 500; - color: var(--text-alt); - transition: color var(--duration-fast) ease, background-color var(--duration-fast) ease; -} - -.mode-group label:hover { - color: var(--text-default); -} - -.mode-group input { display: none; } - -.mode-group input:checked + span { - background: var(--background-accent); - color: var(--text-inverse); - padding: 4px 12px; - border-radius: var(--radius-pill); - margin: -4px -12px; - font-weight: 600; -} - -/* ─── Tile grid (batch) ─── */ - -.grid { - display: grid; - gap: 20px; -} - -.grid.layout-grid { - grid-template-columns: 1fr 1fr; -} - -.grid.layout-stacked { - grid-template-columns: minmax(0, 1100px); - justify-content: center; -} - -.tile { - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - overflow: hidden; - box-shadow: var(--shadow-card); - display: flex; - flex-direction: column; -} - -.tile-header { - padding: 14px 18px; - background: var(--background-default); - border-bottom: 1px solid var(--border-default); - font-size: 12px; - line-height: 1.5; - display: flex; - flex-direction: column; - gap: 4px; -} - -.tile-prompt { - color: var(--text-default); - font-weight: 500; - font-size: 13px; - letter-spacing: -0.005em; -} - -.tile-meta { - color: var(--text-muted); - display: flex; - justify-content: space-between; - gap: 8px; - font-family: var(--font-mono); - font-size: 11px; -} - -.tile-meta .status.pending { color: var(--text-muted); } -.tile-meta .status.streaming { color: var(--color-blue-200); font-weight: 600; } -.tile-meta .status.done { color: #2c8a5a; font-weight: 600; } -.tile-meta .status.error { color: var(--color-red-200); font-weight: 600; } - -.tile-body { position: relative; } -.tile iframe { width: 100%; height: 760px; border: 0; display: block; background: var(--background-default); } -.grid.layout-stacked .tile iframe { height: 880px; } - -.tile-overlay { - position: absolute; - inset: 0; - background: rgba(255, 255, 255, 0.88); - display: none; - align-items: center; - justify-content: center; - font-size: 13px; - color: var(--text-alt); - letter-spacing: 0.02em; - pointer-events: none; - font-weight: 500; -} - -.tile-overlay.on { display: flex; } - -.tile-intent { - padding: 8px 14px; - border-top: 1px dashed var(--border-default); - font-family: var(--font-mono); - font-size: 11px; - color: #2c8a5a; - background: var(--background-alt); - display: none; -} - -.tile-intent.on { display: block; } -.tile-intent.err { color: var(--color-red-200); } - -/* ─── Self-test cases ─── */ - -.case { - margin-bottom: 32px; -} - -.case h2 { - margin: 0 0 4px; - font-size: var(--heading-card-size); - line-height: var(--heading-card-line-height); - letter-spacing: var(--heading-card-letter-spacing); - font-weight: var(--heading-card-weight); - color: var(--text-default); -} - -.case p { - margin: 0 0 14px; - font-size: 13px; - color: var(--text-alt); - max-width: 72ch; -} - -.results { - padding: 14px 18px; - font-family: var(--font-mono); - font-size: 12px; - line-height: 1.7; -} - -.results .pass { color: #2c8a5a; } -.results .fail { color: var(--color-red-200); font-weight: 600; } -.results .info { color: var(--text-alt); } - -/* ─── Inline code ─── */ - -code { - font-family: var(--font-mono); -} - -/* ─── Prompt card (Generate) ───────────────────────────────────── - Single rounded surface that holds meta (direction + mode), the - textarea, and the Run button. Replaces the old multi-shape flex - form row. */ - -form.prompt-card { - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - padding: 14px; - display: flex; - flex-direction: column; - gap: 12px; - margin-bottom: 10px; - box-shadow: var(--shadow-card); -} - -.prompt-meta { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; -} - -select.scenario-select { - min-width: 220px; -} - -select.pill-select { - height: 30px; - padding: 0 30px 0 14px; - font-size: 12px; - font-weight: 500; - border-radius: var(--radius-pill); - background-position: right 10px center; - border-color: var(--border-default); -} - -.ghost-target { - height: 30px; - min-width: 180px; - padding: 0 14px; - font-size: 12px; - font-weight: 500; - border-radius: var(--radius-pill); -} - -.ghost-target:disabled, -select.pill-select:disabled { - opacity: 0.45; -} - -.repair-toggle { - display: inline-flex; - align-items: center; - gap: 7px; - height: 30px; - padding: 0 12px; - border: 1px solid var(--border-default); - border-radius: var(--radius-pill); - font-size: 12px; - font-weight: 500; - color: var(--text-alt); - cursor: pointer; -} - -.repair-toggle input { - width: 13px; - height: 13px; - margin: 0; - accent-color: var(--color-gray-900); -} - -.prompt-input { - position: relative; -} - -.prompt-input textarea { - width: 100%; - display: block; - min-height: 88px; - padding: 12px 88px 12px 14px; - border-radius: var(--radius-card-sm); - background: var(--background-default); -} - -button.prompt-submit { - position: absolute; - bottom: 10px; - right: 10px; - height: 36px; - padding: 0 18px; - background: var(--background-accent); - color: var(--text-inverse); - border: 0; - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 13px; - font-weight: 500; - cursor: pointer; - white-space: nowrap; - box-shadow: var(--shadow-btn); - transition: - background-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - opacity var(--duration-fast) ease; -} - -button.prompt-submit:hover:not(:disabled) { - background: var(--color-gray-700); - box-shadow: var(--shadow-elevated); -} - -button.prompt-submit:disabled { - opacity: 0.4; - cursor: not-allowed; - box-shadow: none; -} - -.surface-controls { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; -} - -.contract-summary { - display: flex; - flex-wrap: wrap; - gap: 6px; - align-items: center; - margin: 0 0 10px; -} - -.contract-chip { - display: inline-flex; - align-items: center; - min-height: 26px; - padding: 4px 10px; - border: 1px solid var(--border-default); - border-radius: var(--radius-pill); - background: var(--background-alt); - color: var(--text-alt); - font-family: var(--font-mono); - font-size: 10px; - line-height: 1.2; - white-space: nowrap; -} - -.contract-chip strong { - margin-right: 5px; - font-family: var(--font-sans); - font-weight: 600; - color: var(--text-default); -} - -.contract-chip.good { - border-color: rgba(44, 138, 90, 0.35); - color: #2c8a5a; -} - -.contract-chip.warn { - border-color: rgba(249, 75, 75, 0.35); - color: var(--text-danger); -} - -.edit-card { - display: grid; - grid-template-columns: minmax(150px, 220px) 1fr auto; - gap: 8px; - align-items: stretch; - margin: 0 0 10px; -} - -.edit-card input, -.edit-card textarea { - min-height: 38px; - height: 38px; - padding: 8px 12px; - border-radius: var(--radius-card-sm); - font-size: 13px; -} - -.edit-card textarea { - resize: vertical; -} - -button.edit-submit { - height: 38px; - padding: 0 16px; - background: var(--background-default); - color: var(--text-default); - border: 1px solid var(--border-input); - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 13px; - font-weight: 500; - cursor: pointer; -} - -button.edit-submit:hover:not(:disabled) { - background: var(--background-muted); - border-color: var(--border-strong); -} - -@media (max-width: 720px) { - .edit-card { - grid-template-columns: 1fr; - } -} - -/* ─── Safety links ─── */ - -.safety-links { - display: flex; - flex-wrap: wrap; - gap: 6px; - align-items: center; - margin: 0 0 20px; -} - -.safety-links a { - display: inline-flex; - align-items: center; - height: 28px; - padding: 0 12px; - background: var(--background-muted); - color: var(--text-alt); - border: 1px solid transparent; - border-radius: var(--radius-pill); - font-size: 12px; - font-weight: 500; - text-decoration: none; -} - -.safety-links a:hover { - background: var(--background-default); - border-color: var(--border-input-hover); - color: var(--text-default); -} - -/* ─── Samples row (legacy compact chips) ─── */ - -.samples-row { - display: flex; - flex-wrap: wrap; - gap: 6px; - align-items: center; - margin-bottom: 20px; -} - -button.btn-chip-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: var(--background-default); - color: var(--text-alt); - border: 1px solid var(--border-input); - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 13px; - cursor: pointer; - transition: - background-color var(--duration-fast) ease, - border-color var(--duration-fast) ease, - color var(--duration-fast) ease; -} - -button.btn-chip-icon:hover:not(:disabled) { - background: var(--background-muted); - border-color: var(--border-strong); - color: var(--text-default); -} - -button.btn-chip-icon:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Sample chips render in DOM order (0). Action buttons are pushed to - the visual end via order, with margin-left:auto on the first to gap - the chips group from the actions group. */ -.samples-row .sample-action { order: 100; } -.samples-row .sample-action:first-of-type { margin-left: auto; } - -details.saved-surfaces { - margin: -8px 0 20px; - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - overflow: hidden; -} - -details.saved-surfaces > summary { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - cursor: pointer; - list-style: none; - font-size: 12px; - font-weight: 600; - color: var(--text-alt); - text-transform: uppercase; -} - -details.saved-surfaces > summary::-webkit-details-marker { - display: none; -} - -#saved-count { - margin-left: auto; - font-family: var(--font-mono); - color: var(--text-muted); -} - -.saved-list { - display: grid; - gap: 1px; - border-top: 1px solid var(--border-default); - background: var(--border-default); -} - -.saved-item { - display: grid; - grid-template-columns: 1fr auto; - gap: 10px; - align-items: center; - padding: 10px 14px; - background: var(--background-default); -} - -.saved-item-title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 13px; - color: var(--text-default); -} - -.saved-item-meta { - margin-top: 2px; - font-family: var(--font-mono); - font-size: 10px; - color: var(--text-muted); -} - -.saved-item button { - height: 28px; - padding: 0 12px; - border: 1px solid var(--border-input); - border-radius: var(--radius-pill); - background: var(--background-default); - color: var(--text-alt); - font: inherit; - font-size: 12px; - cursor: pointer; -} - -.saved-item button:hover { - background: var(--background-muted); - color: var(--text-default); -} - -/* ─── Result pane + welcome overlay ─── */ - -.pane.pane-result { - margin-bottom: 10px; -} - -.iframe-wrap { - position: relative; -} - -.iframe-welcome { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 14px; - background: var(--background-default); - pointer-events: none; - transition: opacity 0.3s ease; - z-index: 1; -} - -.iframe-welcome.hidden { - opacity: 0; -} - -.welcome-icon { - width: 48px; - height: 48px; - border-radius: 16px; - background: var(--background-muted); - color: var(--text-alt); - display: flex; - align-items: center; - justify-content: center; -} - -.welcome-icon svg { - width: 24px; - height: 24px; -} - -.welcome-text { - font-size: 14px; - color: var(--text-muted); - line-height: 1.5; - max-width: min(420px, calc(100% - 48px)); - text-align: center; - letter-spacing: 0; -} - -/* ─── Children stack (summoned sibling sandboxes) ─────────────── - Each child is its own iframe with its own PolicyEngine and state. - The parent emits a `summon` intent; the host appends a card here. */ - -.children-stack { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 10px; -} - -.child-pane { - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - overflow: hidden; -} - -.child-pane > header { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 14px; - border-bottom: 1px solid var(--border-default); - font-size: 13px; - color: var(--text-alt); -} - -.child-pane > header .child-title { - font-weight: 600; - color: var(--text-default); -} - -.child-pane > header .child-prompt { - color: var(--text-muted); - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; -} - -.child-pane > header .child-status { - font-family: var(--font-mono); - font-size: 11px; - color: var(--text-muted); -} - -.child-pane > header .child-close { - background: transparent; - border: 1px solid var(--border-default); - border-radius: 6px; - width: 24px; - height: 24px; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: var(--text-muted); -} - -.child-pane > header .child-close:hover { - color: var(--text-default); - border-color: var(--text-muted); -} - -.child-pane iframe { - width: 100%; - border: 0; - display: block; - background: var(--background-default); - height: 480px; -} - -/* ─── Stream drawer ───────────────────────────────────────────── - Collapsible
that demotes the protocol log from a peer - pane into a debug surface below the result. Closed by default. */ - -details.stream-drawer { - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - overflow: hidden; -} - -details.stream-drawer > summary { - display: flex; - align-items: center; - gap: 12px; - padding: 12px 18px; - cursor: pointer; - list-style: none; - user-select: none; -} - -details.stream-drawer > summary::-webkit-details-marker { - display: none; -} - -details.stream-drawer .stream-label { - font-size: var(--label-size); - letter-spacing: var(--label-letter-spacing); - font-weight: var(--label-weight); - line-height: var(--label-line-height); - text-transform: uppercase; - color: var(--text-alt); -} - -details.stream-drawer .stream-tail { - flex: 1; - font-family: var(--font-mono); - font-size: 11px; - font-weight: 500; - color: var(--text-muted); -} - -details.stream-drawer .stream-arrow { - font-size: 14px; - color: var(--text-muted); - transition: transform var(--duration-fast) ease; -} - -details.stream-drawer[open] .stream-arrow { - transform: rotate(180deg); -} - -details.stream-drawer #log { - max-height: 360px; - border-top: 1px solid var(--border-default); -} - -.devtools-log .ev { - display: grid; - grid-template-columns: 56px 140px 1fr; - gap: 12px; - padding: 2px 0; -} -.devtools-log .ev .ev-time { color: var(--text-muted); } -.devtools-log .ev .ev-kind { font-weight: 600; } -.devtools-log .ev.ev-sandbox-spawned .ev-kind, -.devtools-log .ev.ev-sandbox-ready .ev-kind { color: #2c8a5a; } -.devtools-log .ev.ev-sandbox-fatal .ev-kind, -.devtools-log .ev.ev-sandbox-disposed .ev-kind { color: var(--color-red-200); } -.devtools-log .ev.ev-intent-emitted .ev-kind, -.devtools-log .ev.ev-intent-dispatched .ev-kind { color: var(--color-blue-200); } -.devtools-log .ev.ev-intent-rejected .ev-kind { color: var(--color-red-200); } -.devtools-log .ev.ev-intent-settled .ev-kind { color: var(--text-alt); } -.devtools-log .ev.ev-state-pushed .ev-kind { color: var(--color-blue-200); } -.devtools-log .ev.ev-protocol-line .ev-kind, -.devtools-log .ev.ev-stream-lifecycle .ev-kind, -.devtools-log .ev.ev-render .ev-kind { color: var(--text-muted); } -.devtools-log .ev.ev-protocol-parse-error .ev-kind { color: var(--color-red-200); } - -/* ─── Landing ─── */ - -body.landing { - max-width: none; - padding: 0; - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.landing-main { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 80px 24px; -} - -.landing-content { - width: 100%; - max-width: 720px; -} - -.landing-header { - margin-bottom: 36px; -} - -h1.landing-title { - margin: 0 0 10px; - font-size: clamp(56px, 7vw, 88px); - line-height: 0.92; - letter-spacing: -0.045em; - font-weight: 700; - color: var(--text-default); -} - -p.landing-tagline { - margin: 0; - color: var(--text-alt); - font-size: 15px; - line-height: 1.55; - max-width: 56ch; -} - -.landing-grid { - display: grid; - gap: 12px; - grid-template-columns: 1fr 1fr; -} - -a.landing-card { - display: flex; - flex-direction: column; - gap: 16px; - padding: 24px; - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - text-decoration: none; - color: inherit; - transition: - border-color var(--duration-fast) ease, - box-shadow var(--duration-fast) ease, - transform var(--duration-fast) ease; -} - -a.landing-card:hover { - border-color: var(--border-input-hover); - box-shadow: var(--shadow-elevated); -} - -.landing-card-icon { - width: 40px; - height: 40px; - border-radius: 12px; - background: var(--background-muted); - color: var(--text-default); - display: flex; - align-items: center; - justify-content: center; - transition: - background-color var(--duration-fast) ease, - color var(--duration-fast) ease; -} - -.landing-card-icon svg { - width: 20px; - height: 20px; -} - -a.landing-card:hover .landing-card-icon { - background: var(--background-accent); - color: var(--text-inverse); -} - -.landing-card-body h2 { - margin: 0 0 6px; - font-size: 16px; - font-weight: 600; - letter-spacing: -0.005em; - color: var(--text-default); -} - -.landing-card-body p { - margin: 0; - font-size: 13px; - line-height: 1.55; - color: var(--text-alt); -} - -.landing-card-cta { - display: flex; - align-items: center; - gap: 4px; - font-size: 13px; - font-weight: 500; - color: var(--text-muted); - transition: color var(--duration-fast) ease; - margin-top: auto; -} - -.landing-card-cta .arrow { - width: 14px; - height: 14px; - transition: transform var(--duration-fast) ease; -} - -a.landing-card:hover .landing-card-cta { - color: var(--text-default); -} - -a.landing-card:hover .landing-card-cta .arrow { - transform: translateX(2px); -} - -@media (max-width: 600px) { - .landing-grid { grid-template-columns: 1fr; } -} - -/* ─── Generate workbench ─────────────────────────────────────── */ - -body.generate-page { - max-width: none; - padding: 32px; - background: var(--background-alt); - letter-spacing: 0; -} - -.generate-page .summon-nav, -.generate-header, -.generate-shell, -.diagnostics-dock { - width: min(100%, 1480px); - margin-left: auto; - margin-right: auto; -} - -.generate-page .summon-nav { - margin-bottom: 20px; -} - -.generate-page .page-title { - margin: 0; - font-size: 32px; - line-height: 1.1; - letter-spacing: 0; -} - -.generate-header { - display: flex; - align-items: end; - justify-content: space-between; - gap: 20px; - margin-bottom: 18px; -} - -.generate-kicker { - margin: 0 0 4px; - color: var(--text-alt); - font-size: 13px; -} - -.generate-shell { - display: grid; - grid-template-columns: minmax(220px, 250px) minmax(600px, 1fr) minmax(260px, 310px); - gap: 14px; - align-items: start; -} - -.scenario-rail, -.contract-inspector, -.diagnostics-dock, -.stage-context, -.generate-page form.prompt-card, -.result-toolbar, -.generate-page .edit-card, -.generate-page .pane { - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: 8px; - box-shadow: none; -} - -.scenario-rail, -.contract-inspector { - padding: 14px; - position: sticky; - top: 16px; -} - -.rail-heading, -.inspector-heading { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - margin-bottom: 12px; - color: var(--text-alt); - font-size: var(--label-size); - font-weight: var(--label-weight); - letter-spacing: 0; - line-height: var(--label-line-height); - text-transform: uppercase; -} - -.rail-heading span:last-child, -.inspector-heading span:last-child { - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 11px; - text-transform: none; -} - -.field-label { - display: block; - margin: 0 0 6px; - color: var(--text-muted); - font-size: 11px; - font-weight: 600; - letter-spacing: 0; - text-transform: uppercase; -} - -.generate-page select.scenario-select { - width: 100%; - min-width: 0; - margin-bottom: 12px; -} - -.scenario-list { - display: grid; - gap: 14px; - max-height: calc(100vh - 190px); - overflow: auto; - padding-right: 2px; -} - -.scenario-group { - display: grid; - gap: 6px; -} - -.scenario-group h3 { - margin: 0; - color: var(--text-muted); - font-size: 11px; - font-weight: 600; - letter-spacing: 0; - text-transform: uppercase; -} - -.scenario-card { - display: grid; - gap: 4px; - width: 100%; - padding: 10px; - text-align: left; - color: var(--text-default); - background: var(--background-default); - border: 1px solid var(--border-default); - border-radius: 8px; - font: inherit; - cursor: pointer; - transition: - background-color var(--duration-fast) ease, - border-color var(--duration-fast) ease; -} - -.scenario-card:hover { - border-color: var(--border-input-hover); - background: var(--background-alt); -} - -.scenario-card.active { - border-color: var(--border-strong); - background: var(--background-muted); -} - -.scenario-card-title { - font-size: 13px; - font-weight: 600; -} - -.scenario-card-desc { - color: var(--text-alt); - font-size: 12px; - line-height: 1.35; -} - -.scenario-card-meta { - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 10px; - line-height: 1.4; -} - -.generation-stage { - display: grid; - gap: 12px; - min-width: 0; -} - -.stage-context { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 16px; - align-items: center; - padding: 16px; -} - -.stage-eyebrow, -.toolbar-label { - color: var(--text-muted); - font-size: 11px; - font-weight: 600; - letter-spacing: 0; - text-transform: uppercase; -} - -.stage-context h2 { - margin: 2px 0 4px; - font-size: 20px; - font-weight: 650; - letter-spacing: 0; - line-height: 1.2; -} - -.stage-context p { - margin: 0; - color: var(--text-alt); - font-size: 13px; -} - -.stage-fingerprint { - display: grid; - gap: 4px; - justify-items: end; - min-width: 190px; - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 11px; -} - -.stage-fingerprint strong { - color: var(--text-default); - font-family: var(--font-sans); - font-size: 12px; - font-weight: 600; -} - -.generate-page form.prompt-card { - margin: 0; - padding: 14px; - gap: 8px; -} - -.generate-page .prompt-input textarea { - min-height: 112px; - padding: 14px 104px 14px 14px; - border-radius: 8px; -} - -.generate-page button.prompt-submit { - bottom: 12px; - right: 12px; - height: 40px; - min-width: 78px; - border-radius: 8px; - font-size: 14px; - font-weight: 600; -} - -.result-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 10px 12px; -} - -.result-toolbar[hidden], -.generate-page .edit-card[hidden] { - display: none; -} - -.result-toolbar strong { - display: block; - margin-top: 2px; - color: var(--text-default); - font-family: var(--font-mono); - font-size: 11px; - font-weight: 500; -} - -.toolbar-actions { - display: flex; - gap: 6px; -} - -.toolbar-actions button, -.diagnostics-tabs button, -.generate-page button.edit-submit { - height: 32px; - padding: 0 12px; - border: 1px solid var(--border-input); - border-radius: 8px; - background: var(--background-default); - color: var(--text-alt); - font: inherit; - font-size: 12px; - font-weight: 600; - cursor: pointer; -} - -.toolbar-actions button:hover, -.diagnostics-tabs button:hover, -.generate-page button.edit-submit:hover:not(:disabled) { - border-color: var(--border-strong); - color: var(--text-default); -} - -.generate-page .edit-card { - grid-template-columns: minmax(140px, 220px) 1fr auto; - margin: 0; - padding: 10px; - gap: 8px; -} - -.generate-page .edit-card input, -.generate-page .edit-card textarea { - border-radius: 8px; -} - -.generate-page .pane > header { - padding: 12px 14px; - letter-spacing: 0; -} - -.sandbox-stage { - overflow: hidden; -} - -.generate-page .pane iframe.h-640 { - height: min(62vh, 640px); - min-height: 460px; -} - -.generate-page .iframe-welcome { - background: var(--background-default); -} - -.generate-page .welcome-text { - color: var(--text-muted); - font-size: 13px; -} - -.generate-page .children-stack { - margin: 0; -} - -.contract-summary { - display: grid; - gap: 6px; - margin: 0 0 14px; -} - -.contract-row { - display: grid; - grid-template-columns: 94px minmax(0, 1fr); - gap: 10px; - align-items: start; - min-height: 34px; - padding: 8px 10px; - border: 1px solid var(--border-default); - border-radius: 8px; - background: var(--background-default); -} - -.contract-row-label { - color: var(--text-muted); - font-size: 11px; - font-weight: 600; -} - -.contract-row-value { - min-width: 0; - color: var(--text-default); - font-family: var(--font-mono); - font-size: 11px; - font-weight: 500; - overflow-wrap: anywhere; -} - -.contract-row.good { - border-color: rgba(44, 138, 90, 0.35); - background: rgba(44, 138, 90, 0.06); -} - -.contract-row.warn { - border-color: rgba(249, 75, 75, 0.32); - background: rgba(249, 75, 75, 0.06); -} - -.contract-row.pending { - background: var(--background-alt); -} - -.run-settings { - display: grid; - gap: 12px; - padding-top: 14px; - border-top: 1px solid var(--border-default); -} - -.settings-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; -} - -.settings-grid label, -.ghost-controls label { - min-width: 0; -} - -.generate-page select.pill-select, -.generate-page .ghost-target { - width: 100%; - min-width: 0; - border-radius: 8px; -} - -.settings-row { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} - -.generate-page .mode-group, -.generate-page .repair-toggle, -.custom-contract-toggle { - border-radius: 8px; -} - -.generate-page .mode-group label, -.generate-page .mode-group input:checked + span { - border-radius: 6px; -} - -.ghost-controls { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; -} - -.custom-contract { - margin-top: 14px; - padding-top: 14px; - border-top: 1px solid var(--border-default); -} - -.custom-contract-toggle { - display: flex; - align-items: center; - gap: 8px; - min-height: 34px; - padding: 0 10px; - border: 1px solid var(--border-default); - color: var(--text-alt); - font-size: 12px; - font-weight: 600; - cursor: pointer; -} - -.custom-contract-toggle input { - width: 13px; - height: 13px; - margin: 0; - accent-color: var(--color-gray-900); -} - -.custom-contract-panel { - margin-top: 10px; -} - -.custom-contract-panel[hidden] { - display: none; -} - -.generate-page .surface-controls { - display: grid; - grid-template-columns: 1fr; - gap: 8px; -} - -.diagnostics-dock { - margin-top: 16px; - border-radius: 8px; - overflow: hidden; -} - -.diagnostics-tabs { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 10px; - border-bottom: 1px solid var(--border-default); - background: var(--background-default); -} - -.diagnostics-tabs button.active { - border-color: var(--border-strong); - background: var(--background-accent); - color: var(--text-inverse); -} - -.diagnostics-tabs button span { - margin-left: 6px; - color: currentColor; - font-family: var(--font-mono); - font-size: 10px; - font-weight: 500; - opacity: 0.75; -} - -.diagnostics-panel { - background: var(--background-default); -} - -.diagnostics-panel[hidden] { - display: none; -} - -.diagnostics-panel .log { - max-height: 360px; -} - -.saved-surfaces { - background: var(--background-default); -} - -.saved-list { - background: var(--background-default); - border-top: 0; -} - -.saved-item { - border-bottom: 1px solid var(--border-default); -} - -.saved-item:last-child { - border-bottom: 0; -} - -.saved-item button { - border-radius: 8px; -} - -.generate-page .safety-links { - margin: 0; - padding: 14px; -} - -.generate-page .safety-links a { - border-radius: 8px; -} - -@media (max-width: 1180px) { - .generate-shell { - grid-template-columns: minmax(220px, 260px) minmax(0, 1fr); - } - - .contract-inspector { - grid-column: 1 / -1; - position: static; - } -} - -@media (max-width: 820px) { - body.generate-page { - padding: 20px 14px 56px; - } - - .generate-header { - display: block; - } - - .generate-shell { - grid-template-columns: 1fr; - } - - .scenario-rail { - position: static; - } - - .scenario-list { - max-height: none; - } - - .stage-context { - grid-template-columns: 1fr; - } - - .stage-fingerprint { - justify-items: start; - min-width: 0; - } - - .settings-grid, - .ghost-controls, - .generate-page .edit-card { - grid-template-columns: 1fr; - } - - .generate-page .prompt-input textarea { - padding-right: 14px; - padding-bottom: 58px; - } - - .generate-page .pane iframe.h-640 { - min-height: 420px; - } -} diff --git a/apps/demo/src/App.tsx b/apps/demo/src/App.tsx new file mode 100644 index 0000000..c6c1a6d --- /dev/null +++ b/apps/demo/src/App.tsx @@ -0,0 +1,46 @@ +import { lazy, Suspense } from 'react'; +import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; +import { cn } from './lib/cn.js'; +import { LandingPage } from './pages/LandingPage.js'; +import { ThemeProvider, ThemeToggle } from './theme.js'; + +const AdversarialPage = lazy(() => import('./pages/AdversarialPage.js').then((module) => ({ default: module.AdversarialPage }))); +const BatchPage = lazy(() => import('./pages/BatchPage.js').then((module) => ({ default: module.BatchPage }))); +const FatalPage = lazy(() => import('./pages/FatalPage.js').then((module) => ({ default: module.FatalPage }))); +const FragmentComparePage = lazy(() => import('./pages/FragmentComparePage.js').then((module) => ({ default: module.FragmentComparePage }))); +const GeneratePage = lazy(() => import('./pages/generate/GeneratePage.js').then((module) => ({ default: module.GeneratePage }))); +const StrictPage = lazy(() => import('./pages/StrictPage.js').then((module) => ({ default: module.StrictPage }))); + +function AppRoutes() { + const { pathname } = useLocation(); + const isLanding = pathname === '/'; + + return ( +
+ + Loading...
}> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); +} + +export function App() { + return ( + + + + ); +} diff --git a/apps/demo/src/batch-main.ts b/apps/demo/src/batch-main.ts deleted file mode 100644 index f64b841..0000000 --- a/apps/demo/src/batch-main.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { PolicyEngine, type CapabilityPack } from '@anarchitecture/summon'; -import { spawnSandbox, type SandboxHandle } from '@anarchitecture/summon/browser'; -import { - parseProtocolLine, - SectionAccumulator, - type ValidationCapability, -} from '@anarchitecture/summon/engine'; -import bootstrapSource from '@anarchitecture/summon/bootstrap.js?raw'; -import defaultTokensSource from '@anarchitecture/summon/tokens.css?raw'; -import { ALL_PROMPTS, sample } from './prompts.js'; -import { createDemoCapabilityRegistry } from './capabilities.js'; - -function escapeHtml(s: string): string { - return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"'); -} - -interface DirectionInfo { - id: string; - name: string; - description: string; - tokensCss: string; -} - -type SourceMode = 'random' | 'same'; -type Interactivity = 'static' | 'interactive'; - -/** When interactive, each tile fans out an LLM stream plus on-demand Haiku calls - * on intents. Cap lower than static to keep the cost/latency sane. */ -const MAX_INTERACTIVE_TILES = 8; -const MAX_STATIC_TILES = 12; - -const directionSel = document.getElementById('direction') as HTMLSelectElement; -const countInput = document.getElementById('count') as HTMLInputElement; -const seedInput = document.getElementById('seed') as HTMLInputElement; -const seedWrap = document.getElementById('seed-wrap')!; -const sameWrap = document.getElementById('same-wrap')!; -const samePromptEl = document.getElementById('same-prompt') as HTMLTextAreaElement; -const runBtn = document.getElementById('run') as HTMLButtonElement; -const stopBtn = document.getElementById('stop') as HTMLButtonElement; -const grid = document.getElementById('grid')!; -const summary = document.getElementById('summary')!; - -let directions: DirectionInfo[] = []; -let activeAbort: AbortController | null = null; - -async function loadDirections(): Promise { - try { - const res = await fetch('/api/directions'); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - directions = (await res.json()) as DirectionInfo[]; - } catch { - directions = []; - } - directionSel.innerHTML = ''; - if (directions.length === 0) { - const opt = document.createElement('option'); - opt.value = ''; - opt.textContent = 'Default'; - directionSel.appendChild(opt); - } else { - for (const d of directions) { - const opt = document.createElement('option'); - opt.value = d.id; - opt.textContent = d.name; - opt.title = d.description; - directionSel.appendChild(opt); - } - } -} - -function currentSourceMode(): SourceMode { - const checked = document.querySelector('input[name=mode]:checked'); - return (checked?.value as SourceMode) ?? 'random'; -} -function currentInteractivity(): Interactivity { - const checked = document.querySelector('input[name=interactivity]:checked'); - return (checked?.value as Interactivity) ?? 'static'; -} - -function tokensFor(directionId: string): string { - return directions.find((d) => d.id === directionId)?.tokensCss ?? defaultTokensSource; -} - -document.querySelectorAll('input[name=mode]').forEach((el) => { - el.addEventListener('change', () => { - const m = currentSourceMode(); - seedWrap.style.display = m === 'random' ? '' : 'none'; - sameWrap.style.display = m === 'same' ? 'flex' : 'none'; - }); -}); - -document.querySelectorAll('input[name=interactivity]').forEach((el) => { - el.addEventListener('change', () => { - // Cap count when switching to interactive to avoid surprise fan-out - if (currentInteractivity() === 'interactive') { - const n = Number(countInput.value) || 0; - if (n > MAX_INTERACTIVE_TILES) countInput.value = String(MAX_INTERACTIVE_TILES); - countInput.max = String(MAX_INTERACTIVE_TILES); - } else { - countInput.max = String(MAX_STATIC_TILES); - } - }); -}); - -function applyLayout() { - const val = document.querySelector('input[name=layout]:checked')?.value ?? 'grid'; - grid.classList.toggle('layout-grid', val === 'grid'); - grid.classList.toggle('layout-stacked', val === 'stacked'); -} -document.querySelectorAll('input[name=layout]').forEach((el) => { - el.addEventListener('change', applyLayout); -}); -applyLayout(); - -interface Tile { - prompt: string; - statusEl: HTMLElement; - bytesEl: HTMLElement; - intentEl: HTMLElement; - overlayEl: HTMLElement; - handle: SandboxHandle; - policy: PolicyEngine | null; - capabilityPack: CapabilityPack | null; - validationCapabilities: ValidationCapability[] | null; -} - -let activeTiles: Tile[] = []; - -function disposeTiles() { - for (const t of activeTiles) { - try { - t.handle.dispose(); - } catch { - // ignore - } - } - activeTiles = []; -} - -function makeTile(prompt: string, tokensCss: string, interactivity: Interactivity): Tile { - const el = document.createElement('div'); - el.className = 'tile'; - - const header = document.createElement('div'); - header.className = 'tile-header'; - const promptEl = document.createElement('div'); - promptEl.className = 'tile-prompt'; - promptEl.textContent = prompt; - const metaEl = document.createElement('div'); - metaEl.className = 'tile-meta'; - const statusEl = document.createElement('span'); - statusEl.className = 'status pending'; - statusEl.textContent = 'pending'; - const bytesEl = document.createElement('span'); - bytesEl.textContent = '0 B'; - metaEl.append(statusEl, bytesEl); - header.append(promptEl, metaEl); - - const intentEl = document.createElement('div'); - intentEl.className = 'tile-intent'; - header.appendChild(intentEl); - - const body = document.createElement('div'); - body.className = 'tile-body'; - const iframe = document.createElement('iframe'); - iframe.title = prompt; - const overlayEl = document.createElement('div'); - overlayEl.className = 'tile-overlay'; - overlayEl.textContent = 'Generating…'; - body.append(iframe, overlayEl); - - el.append(header, body); - grid.appendChild(el); - - // Per-tile policy — in interactive mode each tile owns its own handler state - // (counters, chosen options, etc.) so actions in one tile don't bleed into another. - let policy: PolicyEngine | null = null; - let capabilityPack: CapabilityPack | null = null; - let validationCapabilities: ValidationCapability[] | null = null; - if (interactivity === 'interactive') { - const markIntent = (msg: string, err = false) => { - intentEl.textContent = msg; - intentEl.classList.add('on'); - intentEl.classList.toggle('err', err); - }; - const registry = createDemoCapabilityRegistry({ - onLog: (m) => markIntent(m, false), - onError: (m) => markIntent(m, true), - }).without(['summon']); - const contract = registry.toContract(); - capabilityPack = contract.pack; - validationCapabilities = contract.validationCapabilities; - policy = new PolicyEngine({ - initialState: contract.initialState, - handlers: registry.toPolicyHandlers(), - onStateChange: (state) => { - if (handleRef.current) handleRef.current.pushState(state); - }, - onHandlerError: (intent, error) => { - markIntent(`host handler error (${intent}): ${error.message}`, true); - }, - }); - } - - // Capture handle in a ref so onStateChange can reach it without TDZ issues. - const handleRef: { current: SandboxHandle | null } = { current: null }; - - const handle = spawnSandbox({ - iframe, - artifact: { - intents: policy?.intents ?? [], - capabilities: validationCapabilities ?? undefined, - html: '', - initialState: policy?.getState(), - }, - // Per-tile grant comes from the per-tile engine the host built — never - // from the artifact, so a generated tile can't broaden its own access. - grantedIntents: policy?.intents ?? [], - grantedCapabilities: validationCapabilities ?? undefined, - bootstrapSource, - tokensSource: tokensCss, - onIntent: (intent, args) => { - void policy?.dispatch(intent, args); - }, - onIntentRejected: (reason) => { - intentEl.textContent = `request rejected: ${reason}`; - intentEl.classList.add('on', 'err'); - }, - }); - handleRef.current = handle; - - return { - prompt, - statusEl, - bytesEl, - intentEl, - overlayEl, - handle, - policy, - capabilityPack, - validationCapabilities, - }; -} - -async function runOne( - tile: Tile, - directionId: string, - interactivity: Interactivity, - signal: AbortSignal, -): Promise<{ ok: boolean; bytes: number; ms: number }> { - const acc = new SectionAccumulator(); - const start = performance.now(); - let bytes = 0; - tile.statusEl.className = 'status streaming'; - tile.statusEl.textContent = 'streaming'; - tile.overlayEl.classList.toggle('on', interactivity === 'interactive'); - - // Interactive = batched render (scripts need full DOM). Static = live paint. - const renderIncrementally = interactivity === 'static'; - - try { - const res = await fetch('/api/generate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - prompt: tile.prompt, - directionId, - mode: interactivity, - capabilities: interactivity === 'interactive' ? tile.capabilityPack : undefined, - }), - signal, - }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const reader = res.body?.getReader(); - if (!reader) throw new Error('no body'); - - const decoder = new TextDecoder(); - let buffer = ''; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - bytes += value.byteLength; - tile.bytesEl.textContent = `${bytes.toLocaleString()} B`; - buffer += decoder.decode(value, { stream: true }); - let nl = buffer.indexOf('\n'); - while (nl !== -1) { - const line = buffer.slice(0, nl); - buffer = buffer.slice(nl + 1); - const parsed = parseProtocolLine(line); - if (parsed) { - const changed = acc.apply(parsed); - if (renderIncrementally && changed && acc.hasAnySection()) { - tile.handle.render(acc.compose()); - } - } - nl = buffer.indexOf('\n'); - } - } - const tail = buffer.trim(); - if (tail) { - const parsed = parseProtocolLine(tail); - if (parsed) { - const changed = acc.apply(parsed); - if (renderIncrementally && changed && acc.hasAnySection()) { - tile.handle.render(acc.compose()); - } - } - } - - // Batched render for interactive — scripts get the full DOM in one shot. - if (!renderIncrementally && acc.hasAnySection()) { - tile.handle.render(acc.compose()); - } - - tile.overlayEl.classList.remove('on'); - - const ms = Math.round(performance.now() - start); - tile.statusEl.className = 'status done'; - tile.statusEl.textContent = `${(ms / 1000).toFixed(1)}s`; - tile.bytesEl.textContent = `${bytes.toLocaleString()} B`; - return { ok: true, bytes, ms }; - } catch (err) { - tile.overlayEl.classList.remove('on'); - if ((err as Error).name === 'AbortError') { - tile.statusEl.className = 'status error'; - tile.statusEl.textContent = 'aborted'; - return { ok: false, bytes, ms: Math.round(performance.now() - start) }; - } - const msg = err instanceof Error ? err.message : String(err); - tile.statusEl.className = 'status error'; - tile.statusEl.textContent = `error: ${msg.slice(0, 40)}`; - return { ok: false, bytes, ms: Math.round(performance.now() - start) }; - } -} - -async function run() { - // Dispose prior tiles (clean up message listeners) before clearing the grid. - disposeTiles(); - grid.innerHTML = ''; - - const directionId = directionSel.value; - const interactivity = currentInteractivity(); - const cap = interactivity === 'interactive' ? MAX_INTERACTIVE_TILES : MAX_STATIC_TILES; - const count = Math.max(1, Math.min(cap, Number(countInput.value) || 1)); - const sourceMode = currentSourceMode(); - - let prompts: string[]; - if (sourceMode === 'same') { - const p = samePromptEl.value.trim(); - if (!p) { - summary.textContent = 'Enter a prompt for Same mode.'; - return; - } - prompts = new Array(count).fill(p); - } else { - const seedStr = seedInput.value.trim(); - const seed = seedStr ? Number(seedStr) : ((Date.now() & 0x7fffffff) | 0); - if (!seedStr) seedInput.placeholder = String(seed); - prompts = sample(ALL_PROMPTS, count, seed); - summary.textContent = `Running ${count} (${interactivity}) with seed ${seed}…`; - } - - const tokensCss = tokensFor(directionId); - const tiles = prompts.map((p) => makeTile(p, tokensCss, interactivity)); - activeTiles = tiles; - - activeAbort = new AbortController(); - runBtn.disabled = true; - stopBtn.disabled = false; - - const runStart = performance.now(); - const results = await Promise.all( - tiles.map((t) => runOne(t, directionId, interactivity, activeAbort!.signal)), - ); - const wall = Math.round(performance.now() - runStart); - - const ok = results.filter((r) => r.ok).length; - const failed = results.length - ok; - const totalBytes = results.reduce((a, r) => a + r.bytes, 0); - const avgMs = Math.round(results.reduce((a, r) => a + r.ms, 0) / results.length); - const seedNote = - sourceMode === 'random' ? ` · seed ${escapeHtml(seedInput.value || seedInput.placeholder)}` : ''; - const modeNote = interactivity === 'interactive' ? ' · interactive' : ''; - summary.innerHTML = `Done in ${(wall / 1000).toFixed(1)}s wall. ${ok} ok · ${failed} failed · avg per-tile ${(avgMs / 1000).toFixed(1)}s · ${totalBytes.toLocaleString()} bytes total${modeNote}${seedNote}.`; - - runBtn.disabled = false; - stopBtn.disabled = true; - activeAbort = null; -} - -runBtn.addEventListener('click', () => { - void run(); -}); -stopBtn.addEventListener('click', () => { - activeAbort?.abort(); -}); - -void loadDirections(); diff --git a/apps/demo/src/components.ts b/apps/demo/src/components.ts deleted file mode 100644 index 0f5b9c7..0000000 --- a/apps/demo/src/components.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - createComponentRegistry, - defineComponent, - type ComponentDefinition, - type ComponentRegistry, -} from '@anarchitecture/summon'; -import type { ComponentPack } from '@anarchitecture/summon'; -import { z } from 'zod'; - -const metricCardPropsSchema = z.object({ - label: z.string(), - value: z.string(), - delta: z.string().optional(), - tone: z.enum(['neutral', 'good', 'warn']).optional(), -}); - -const trendSparklinePropsSchema = z.object({ - label: z.string(), - points: z.array(z.number()).min(2).max(12), - caption: z.string().optional(), -}); - -const approvalStatusPropsSchema = z.object({ - status: z.enum(['pending', 'approved', 'blocked']), - title: z.string(), - detail: z.string().optional(), -}); - -type MetricCardProps = z.infer; -type TrendSparklineProps = z.infer; -type ApprovalStatusProps = z.infer; - -export function createDemoComponentRegistry(componentNames?: readonly string[]): ComponentRegistry { - const allowed = componentNames ? new Set(componentNames) : null; - const definitions = demoComponentDefinitions().filter((definition) => - allowed ? allowed.has(definition.name) : true, - ); - return createComponentRegistry(definitions); -} - -export function baseDemoComponentPack(): ComponentPack { - return createDemoComponentRegistry().toContract().pack; -} - -export function narrowComponentPack(pack: ComponentPack, componentNames: readonly string[]): ComponentPack { - const allowed = new Set(componentNames); - return { - components: pack.components.filter((component) => allowed.has(component.name)), - }; -} - -function demoComponentDefinitions(): ComponentDefinition[] { - return [ - defineComponent({ - name: 'MetricCard', - description: - 'Displays one compact KPI with an optional delta and tone. Use for launch metrics, readiness scores, revenue, risk, or progress numbers.', - propsSchema: metricCardPropsSchema, - sizing: { height: 'var(--space-10)', description: 'Works well in a 2-4 column metric grid.' }, - examples: [ - { - name: 'KPI placeholder', - code: `
`, - }, - ], - render: ({ container, props }) => { - const tone = props.tone ?? 'neutral'; - container.innerHTML = ` -
-
${esc(props.label)}
-
- ${esc(props.value)} - ${props.delta ? `${esc(props.delta)}` : ''} -
-
`; - }, - }), - defineComponent({ - name: 'TrendSparkline', - description: - 'Displays a small trend line from numeric points. Use when a generated surface needs a compact visual trend instead of a text-only metric.', - propsSchema: trendSparklinePropsSchema, - sizing: { height: 'var(--space-11)', description: 'Needs enough height for the chart and caption.' }, - examples: [ - { - name: 'Trend placeholder', - code: `
`, - }, - ], - render: ({ container, props }) => { - const points = props.points.length >= 2 ? props.points : [0, 0]; - const min = Math.min(...points); - const max = Math.max(...points); - const spread = max - min || 1; - const d = points.map((point, index) => { - const x = (index / Math.max(points.length - 1, 1)) * 220 + 10; - const y = 74 - ((point - min) / spread) * 54 + 10; - return `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`; - }).join(' '); - container.innerHTML = ` -
-
- ${esc(props.label)} - ${points.length} pts -
- - - - - ${props.caption ? `
${esc(props.caption)}
` : ''} -
`; - }, - }), - defineComponent({ - name: 'ApprovalStatus', - description: - 'Displays a launch or publish approval state with a strong status treatment. Use for pending, approved, or blocked readiness gates.', - propsSchema: approvalStatusPropsSchema, - sizing: { height: 'var(--space-9)', description: 'Fits a compact status row or card.' }, - examples: [ - { - name: 'Approval placeholder', - code: `
`, - }, - ], - render: ({ container, props }) => { - const colors = { - pending: ['#fff7ed', '#cc4b03', 'Pending'], - approved: ['#f2fff6', '#008c2e', 'Approved'], - blocked: ['#fff1f2', '#cc0023', 'Blocked'], - } as const; - const [bg, fg, label] = colors[props.status]; - container.innerHTML = ` -
- ${label} - ${esc(props.title)} - ${props.detail ? `${esc(props.detail)}` : ''} -
`; - }, - }), - ]; -} - -function esc(value: unknown): string { - return String(value ?? '').replace(/[&<>"']/g, (char) => ({ - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - }[char] ?? char)); -} diff --git a/apps/demo/src/components.tsx b/apps/demo/src/components.tsx new file mode 100644 index 0000000..47f8558 --- /dev/null +++ b/apps/demo/src/components.tsx @@ -0,0 +1,194 @@ +import { + createComponentRegistry, + type ComponentDefinition, + type ComponentRegistry, +} from '@anarchitecture/summon'; +import type { ComponentPack } from '@anarchitecture/summon'; +import { defineReactComponent } from '@anarchitecture/summon-react'; +import type { CSSProperties } from 'react'; +import { z } from 'zod'; + +const metricCardPropsSchema = z.object({ + label: z.string(), + value: z.string(), + delta: z.string().optional(), + tone: z.enum(['neutral', 'good', 'warn']).optional(), +}); + +const trendSparklinePropsSchema = z.object({ + label: z.string(), + points: z.array(z.number()).min(2).max(12), + caption: z.string().optional(), +}); + +const approvalStatusPropsSchema = z.object({ + status: z.enum(['pending', 'approved', 'blocked']), + title: z.string(), + detail: z.string().optional(), +}); + +type MetricCardProps = z.infer; +type TrendSparklineProps = z.infer; +type ApprovalStatusProps = z.infer; + +export function createDemoComponentRegistry(componentNames?: readonly string[]): ComponentRegistry { + const allowed = componentNames ? new Set(componentNames) : null; + const definitions = demoComponentDefinitions().filter((definition) => + allowed ? allowed.has(definition.name) : true, + ); + return createComponentRegistry(definitions); +} + +export function baseDemoComponentPack(): ComponentPack { + return createDemoComponentRegistry().toContract().pack; +} + +export function narrowComponentPack(pack: ComponentPack, componentNames: readonly string[]): ComponentPack { + const allowed = new Set(componentNames); + return { + components: pack.components.filter((component) => allowed.has(component.name)), + }; +} + +function demoComponentDefinitions(): ComponentDefinition[] { + return [ + defineReactComponent({ + name: 'MetricCard', + description: + 'Displays one compact KPI with an optional delta and tone. Use for launch metrics, readiness scores, revenue, risk, or progress numbers.', + propsSchema: metricCardPropsSchema, + sizing: { height: 'var(--space-10)', description: 'Works well in a 2-4 column metric grid.' }, + examples: [ + { + name: 'KPI placeholder', + code: `
`, + }, + ], + component: MetricCard, + }), + defineReactComponent({ + name: 'TrendSparkline', + description: + 'Displays a small trend line from numeric points. Use when a generated surface needs a compact visual trend instead of a text-only metric.', + propsSchema: trendSparklinePropsSchema, + sizing: { height: 'var(--space-11)', description: 'Needs enough height for the chart and caption.' }, + examples: [ + { + name: 'Trend placeholder', + code: `
`, + }, + ], + component: TrendSparkline, + }), + defineReactComponent({ + name: 'ApprovalStatus', + description: + 'Displays a launch or publish approval state with a strong status treatment. Use for pending, approved, or blocked readiness gates.', + propsSchema: approvalStatusPropsSchema, + sizing: { height: 'var(--space-9)', description: 'Fits a compact status row or card.' }, + examples: [ + { + name: 'Approval placeholder', + code: `
`, + }, + ], + component: ApprovalStatus, + }), + ]; +} + +function MetricCard({ label, value, delta, tone = 'neutral' }: MetricCardProps) { + const border = tone === 'warn' ? '#cc4b03' : '#e6e6e6'; + const background = tone === 'good' ? '#f2fff6' : tone === 'warn' ? '#fff7ed' : '#fff'; + const deltaColor = tone === 'warn' ? '#cc4b03' : tone === 'good' ? '#008c2e' : '#6b6b6b'; + return ( +
+
{label}
+
+ {value} + {delta ? {delta} : null} +
+
+ ); +} + +function TrendSparkline({ label, points, caption }: TrendSparklineProps) { + const safePoints = points.length >= 2 ? points : [0, 0]; + const min = Math.min(...safePoints); + const max = Math.max(...safePoints); + const spread = max - min || 1; + const d = safePoints.map((point, index) => { + const x = (index / Math.max(safePoints.length - 1, 1)) * 220 + 10; + const y = 74 - ((point - min) / spread) * 54 + 10; + return `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`; + }).join(' '); + return ( +
+
+ {label} + {safePoints.length} pts +
+ + + + + {caption ?
{caption}
: null} +
+ ); +} + +function ApprovalStatus({ status, title, detail }: ApprovalStatusProps) { + const colors = { + pending: ['#fff7ed', '#cc4b03', 'Pending'], + approved: ['#f2fff6', '#008c2e', 'Approved'], + blocked: ['#fff1f2', '#cc0023', 'Blocked'], + } as const; + const [bg, fg, label] = colors[status]; + return ( +
+ {label} + {title} + {detail ? {detail} : null} +
+ ); +} + +const hostCardStyle: CSSProperties = { + height: '100%', + boxSizing: 'border-box', + padding: '14px 16px', + borderRadius: 14, + border: '1px solid #e6e6e6', + color: '#101010', + fontFamily: 'system-ui, -apple-system, Segoe UI, sans-serif', +}; + +const metricLabelStyle: CSSProperties = { + fontSize: 11, + textTransform: 'uppercase', + letterSpacing: '0.06em', + color: '#6b6b6b', + fontWeight: 700, +}; + +const metricValueStyle: CSSProperties = { + fontSize: 34, + lineHeight: 1, + letterSpacing: '-0.03em', +}; + +const metricDeltaStyle: CSSProperties = { + fontSize: 13, + fontWeight: 700, +}; + +const approvalBadgeStyle: CSSProperties = { + width: 'max-content', + padding: '3px 9px', + borderRadius: 999, + color: 'white', + fontSize: 11, + fontWeight: 800, + letterSpacing: '0.04em', + textTransform: 'uppercase', +}; diff --git a/apps/demo/src/components/TrustedFixtureSurface.tsx b/apps/demo/src/components/TrustedFixtureSurface.tsx new file mode 100644 index 0000000..73748d4 --- /dev/null +++ b/apps/demo/src/components/TrustedFixtureSurface.tsx @@ -0,0 +1,231 @@ +import { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, + type CSSProperties, +} from 'react'; +import bootstrapSource from '@anarchitecture/summon/bootstrap.js?raw'; +import tokensSource from '@anarchitecture/summon/tokens.css?raw'; + +export interface TrustedFixtureSurfaceHandle { + iframe: HTMLIFrameElement | null; + sandboxId: string | null; + pushState(state: Record): void; +} + +export interface TrustedFixtureSurfaceProps { + html: string; + grantedIntents: string[]; + initialState?: Record; + onIntent?: (intent: string, args: Record) => void; + onIntentRejected?: (reason: string, raw: unknown) => void; + onFatal?: (reason: string) => void; + id?: string; + title?: string; + className?: string; + style?: CSSProperties; +} + +function randomId(): string { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); +} + +function escapeHtml(s: string): string { + return s + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); +} + +function escapeScript(s: string): string { + return s.replace(/<\/script/gi, '<\\/script'); +} + +function escapeScriptJson(value: unknown): string { + return JSON.stringify(value).replaceAll('<', '\\u003c'); +} + +function cspForNonce(nonce: string): string { + return [ + "default-src 'none'", + `script-src 'nonce-${nonce}'`, + "style-src 'unsafe-inline'", + "img-src data:", + "font-src data:", + "connect-src 'none'", + "form-action 'none'", + "base-uri 'none'", + "frame-src 'none'", + "child-src 'none'", + "media-src 'none'", + "object-src 'none'", + "worker-src 'none'", + ].join('; '); +} + +function nonceFixtureScripts(html: string, nonce: string): string { + return html.replace(/]*\bnonce=)/gi, ` + + + +
${nonceFixtureScripts(params.html, params.nonce)}
+`; +} + +/** + * Demo-only escape hatch for trusted adversarial fixtures. It keeps the sandbox + * and host intent bridge under test without letting generated artifacts regain + * public script execution. + */ +export const TrustedFixtureSurface = forwardRef( + function TrustedFixtureSurface(props, ref) { + const iframeRef = useRef(null); + const sandboxId = useMemo(randomId, []); + const nonce = useMemo(randomId, []); + const readyRef = useRef(false); + const pendingStatesRef = useRef[]>([]); + + function postState(state: Record) { + const iframe = iframeRef.current; + if (!readyRef.current || !iframe?.contentWindow) { + pendingStatesRef.current.push(state); + return; + } + iframe.contentWindow.postMessage({ + type: 'SUMMON_STATE', + sandbox_id: sandboxId, + state, + }, '*'); + } + + useImperativeHandle(ref, () => ({ + get iframe() { + return iframeRef.current; + }, + get sandboxId() { + return sandboxId; + }, + pushState(state: Record) { + postState(state); + }, + }), [sandboxId]); + + useEffect(() => { + readyRef.current = false; + pendingStatesRef.current = props.initialState ? [props.initialState] : []; + const intentAllowlist = new Set(props.grantedIntents); + + function flushPending() { + const iframe = iframeRef.current; + if (!readyRef.current || !iframe?.contentWindow) return; + while (pendingStatesRef.current.length > 0) { + const state = pendingStatesRef.current.shift()!; + iframe.contentWindow.postMessage({ + type: 'SUMMON_STATE', + sandbox_id: sandboxId, + state, + }, '*'); + } + } + + function handleMessage(event: MessageEvent) { + const data = event.data as { + type?: string; + sandbox_id?: string; + reason?: unknown; + intent?: unknown; + args?: unknown; + } | undefined; + if (!data || typeof data !== 'object') return; + if ( + data.type !== 'SUMMON_READY' && + data.type !== 'SUMMON_FATAL' && + data.type !== 'SUMMON_INTENT' + ) { + return; + } + if (data.sandbox_id !== sandboxId) return; + + if (data.type === 'SUMMON_FATAL') { + readyRef.current = false; + props.onFatal?.(typeof data.reason === 'string' ? data.reason : 'unknown'); + return; + } + + if (data.type === 'SUMMON_READY') { + readyRef.current = true; + flushPending(); + return; + } + + const intent = data.intent; + if (typeof intent !== 'string' || !intent) { + props.onIntentRejected?.('intent not a non-empty string', data); + return; + } + if (!intentAllowlist.has(intent)) { + props.onIntentRejected?.(`intent "${intent}" not granted`, data); + return; + } + const args = data.args && typeof data.args === 'object' + ? data.args as Record + : {}; + props.onIntent?.(intent, args); + } + + window.addEventListener('message', handleMessage); + if (iframeRef.current) { + iframeRef.current.setAttribute('sandbox', 'allow-scripts'); + iframeRef.current.srcdoc = buildSrcdoc({ + sandboxId, + nonce, + html: props.html, + }); + } + + return () => { + window.removeEventListener('message', handleMessage); + readyRef.current = false; + pendingStatesRef.current = []; + if (iframeRef.current) iframeRef.current.srcdoc = ''; + }; + }, [ + nonce, + props.grantedIntents, + props.html, + props.initialState, + props.onFatal, + props.onIntent, + props.onIntentRejected, + sandboxId, + ]); + + return ( + - -
-
Host bridge log
-
-
- - - - - diff --git a/apps/demo/tsconfig.json b/apps/demo/tsconfig.json index aaff06f..2fd43be 100644 --- a/apps/demo/tsconfig.json +++ b/apps/demo/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + }, "include": ["src/**/*", "vite.config.ts"] } diff --git a/apps/demo/vite.config.ts b/apps/demo/vite.config.ts index 8dce383..9c7fcc0 100644 --- a/apps/demo/vite.config.ts +++ b/apps/demo/vite.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vite'; -import { resolve } from 'node:path'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ + plugins: [react(), tailwindcss()], server: { port: 5173, strictPort: true, @@ -12,18 +14,4 @@ export default defineConfig({ }, }, }, - build: { - rollupOptions: { - input: { - // Multi-page app — every nav target needs to be a build input or - // production won't ship its bundle. Dev mode picks them up implicitly. - main: resolve(__dirname, 'index.html'), - generate: resolve(__dirname, 'generate.html'), - adversarial: resolve(__dirname, 'adversarial.html'), - batch: resolve(__dirname, 'batch.html'), - strict: resolve(__dirname, 'strict.html'), - fatal: resolve(__dirname, 'fatal.html'), - }, - }, - }, }); diff --git a/apps/server/directions/ghost/bucket.json b/apps/server/directions/ghost/bucket.json deleted file mode 100644 index e2cde45..0000000 --- a/apps/server/directions/ghost/bucket.json +++ /dev/null @@ -1,9041 +0,0 @@ -{ - "schema": "ghost.bucket/v1", - "sources": [ - { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - } - ], - "values": [ - { - "id": "6e69b083282848c1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ffffff", - "raw": "#ffffff", - "spec": { - "space": "srgb", - "hex": "#ffffff" - }, - "occurrences": 24, - "files_count": 3 - }, - { - "id": "e731e23f634deeb6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#000000", - "raw": "#000000", - "spec": { - "space": "srgb", - "hex": "#000000" - }, - "occurrences": 15, - "files_count": 3 - }, - { - "id": "c79bc8c1100da370", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#1a1a1a", - "raw": "#1a1a1a", - "spec": { - "space": "srgb", - "hex": "#1a1a1a" - }, - "occurrences": 9, - "files_count": 3 - }, - { - "id": "3fe5f28d2068d451", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f5f5f5", - "raw": "#f5f5f5", - "spec": { - "space": "srgb", - "hex": "#f5f5f5" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "d7923c1978440a6b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f0f0f0", - "raw": "#f0f0f0", - "spec": { - "space": "srgb", - "hex": "#f0f0f0" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "12ed8b3e398894d4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e8e8e8", - "raw": "#e8e8e8", - "spec": { - "space": "srgb", - "hex": "#e8e8e8" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "7c194b8294f28371", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e5e5e5", - "raw": "#e5e5e5", - "spec": { - "space": "srgb", - "hex": "#e5e5e5" - }, - "occurrences": 2, - "files_count": 2 - }, - { - "id": "6854c28b440d7eb0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#cccccc", - "raw": "#cccccc", - "spec": { - "space": "srgb", - "hex": "#cccccc" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "07174d5713e8b8a4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#999999", - "raw": "#999999", - "spec": { - "space": "srgb", - "hex": "#999999" - }, - "occurrences": 4, - "files_count": 2 - }, - { - "id": "dc9a1238c1c047b3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#666666", - "raw": "#666666", - "spec": { - "space": "srgb", - "hex": "#666666" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "bcd3ef3ff68f9d12", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#333333", - "raw": "#333333", - "spec": { - "space": "srgb", - "hex": "#333333" - }, - "occurrences": 7, - "files_count": 2 - }, - { - "id": "bb902a35264b0890", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#232323", - "raw": "#232323", - "spec": { - "space": "srgb", - "hex": "#232323" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "9c729ed944e666bb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ff6b6b", - "raw": "#ff6b6b", - "spec": { - "space": "srgb", - "hex": "#ff6b6b" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "19c202cebe694001", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f94b4b", - "raw": "#f94b4b", - "spec": { - "space": "srgb", - "hex": "#f94b4b" - }, - "occurrences": 5, - "files_count": 2 - }, - { - "id": "fe0369af3cbbbf9f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7cacff", - "raw": "#7cacff", - "spec": { - "space": "srgb", - "hex": "#7cacff" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "ada542848d652566", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#5c98f9", - "raw": "#5c98f9", - "spec": { - "space": "srgb", - "hex": "#5c98f9" - }, - "occurrences": 5, - "files_count": 2 - }, - { - "id": "ad3c6b705201f129", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a3d795", - "raw": "#a3d795", - "spec": { - "space": "srgb", - "hex": "#a3d795" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "1ddb693061c37cb1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#91cb80", - "raw": "#91cb80", - "spec": { - "space": "srgb", - "hex": "#91cb80" - }, - "occurrences": 9, - "files_count": 2 - }, - { - "id": "50cd39bed6eb5a81", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ffd966", - "raw": "#ffd966", - "spec": { - "space": "srgb", - "hex": "#ffd966" - }, - "occurrences": 3, - "files_count": 2 - }, - { - "id": "248678789c4c4c4c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#fbcd44", - "raw": "#fbcd44", - "spec": { - "space": "srgb", - "hex": "#fbcd44" - }, - "occurrences": 5, - "files_count": 2 - }, - { - "id": "f5cca5fab21e800f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f6b44a", - "raw": "#f6b44a", - "spec": { - "space": "srgb", - "hex": "#f6b44a" - }, - "occurrences": 4, - "files_count": 2 - }, - { - "id": "eb387b5cee1b99f3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7585ff", - "raw": "#7585ff", - "spec": { - "space": "srgb", - "hex": "#7585ff" - }, - "occurrences": 4, - "files_count": 2 - }, - { - "id": "0788fe5e6162d3b3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d76a6a", - "raw": "#d76a6a", - "spec": { - "space": "srgb", - "hex": "#d76a6a" - }, - "occurrences": 4, - "files_count": 2 - }, - { - "id": "92412b315c6b770d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d185e0", - "raw": "#d185e0", - "spec": { - "space": "srgb", - "hex": "#d185e0" - }, - "occurrences": 4, - "files_count": 2 - }, - { - "id": "c6e85fe2a8b3d8ee", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#0a0a0a", - "raw": "#0a0a0a", - "spec": { - "space": "srgb", - "hex": "#0a0a0a" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "80595b0869411ca2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#9c5930", - "raw": "#9C5930", - "spec": { - "space": "srgb", - "hex": "#9c5930" - }, - "occurrences": 4, - "files_count": 1 - }, - { - "id": "55c05e2a5f286a6b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#fbf5ec", - "raw": "#FBF5EC", - "spec": { - "space": "srgb", - "hex": "#fbf5ec" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "427f81de1366d03a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3d2b1f", - "raw": "#3D2B1F", - "spec": { - "space": "srgb", - "hex": "#3d2b1f" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "df44e54dcc7b3473", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f0e4d4", - "raw": "#F0E4D4", - "spec": { - "space": "srgb", - "hex": "#f0e4d4" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "168b80d0977f1637", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f5ecdf", - "raw": "#F5ECDF", - "spec": { - "space": "srgb", - "hex": "#f5ecdf" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "9c16b85eb2fbfe74", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c4a882", - "raw": "#C4A882", - "spec": { - "space": "srgb", - "hex": "#c4a882" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "ab3623c3e9e6095b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e2d4c0", - "raw": "#E2D4C0", - "spec": { - "space": "srgb", - "hex": "#e2d4c0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "4abd9258d241e712", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d4c2aa", - "raw": "#D4C2AA", - "spec": { - "space": "srgb", - "hex": "#d4c2aa" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "dd0267341610080c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#8b7355", - "raw": "#8B7355", - "spec": { - "space": "srgb", - "hex": "#8b7355" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "fba6fa3478179f2e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#6b5940", - "raw": "#6B5940", - "spec": { - "space": "srgb", - "hex": "#6b5940" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "149b18a7142c66e8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d4853a", - "raw": "#D4853A", - "spec": { - "space": "srgb", - "hex": "#d4853a" - }, - "occurrences": 6, - "files_count": 1 - }, - { - "id": "7a1b3568eceeb6ad", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7b9ea8", - "raw": "#7B9EA8", - "spec": { - "space": "srgb", - "hex": "#7b9ea8" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "c7c97b97eea29f8a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c46b5a", - "raw": "#C46B5A", - "spec": { - "space": "srgb", - "hex": "#c46b5a" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "de55418789aaf8e7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a8856b", - "raw": "#A8856B", - "spec": { - "space": "srgb", - "hex": "#a8856b" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "475a2fc46f6d607e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#8baa72", - "raw": "#8BAA72", - "spec": { - "space": "srgb", - "hex": "#8baa72" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "2fd6de9417b57f99", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#1c1410", - "raw": "#1C1410", - "spec": { - "space": "srgb", - "hex": "#1c1410" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "d8240f5bc18c5258", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#2a1f18", - "raw": "#2A1F18", - "spec": { - "space": "srgb", - "hex": "#2a1f18" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "a310ecce7b887c32", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#4a382a", - "raw": "#4A382A", - "spec": { - "space": "srgb", - "hex": "#4a382a" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "e9a94d8c1ad15ead", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3a2c20", - "raw": "#3A2C20", - "spec": { - "space": "srgb", - "hex": "#3a2c20" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "a256d0940d233d19", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#5a4530", - "raw": "#5A4530", - "spec": { - "space": "srgb", - "hex": "#5a4530" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "c0b5d0398d5ac019", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#1b6b8a", - "raw": "#1B6B8A", - "spec": { - "space": "srgb", - "hex": "#1b6b8a" - }, - "occurrences": 4, - "files_count": 1 - }, - { - "id": "313d2e7176c4c395", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f4f9fb", - "raw": "#F4F9FB", - "spec": { - "space": "srgb", - "hex": "#f4f9fb" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "26675007f3e1fe4b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#0f3442", - "raw": "#0F3442", - "spec": { - "space": "srgb", - "hex": "#0f3442" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "288258c0156a3a09", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e4f0f5", - "raw": "#E4F0F5", - "spec": { - "space": "srgb", - "hex": "#e4f0f5" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "34d987fefca1af6a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#eaf3f8", - "raw": "#EAF3F8", - "spec": { - "space": "srgb", - "hex": "#eaf3f8" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "cceb2c3ed330bfdd", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#8bb8ca", - "raw": "#8BB8CA", - "spec": { - "space": "srgb", - "hex": "#8bb8ca" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "cbd41f1cf1a7e3c8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c8dee8", - "raw": "#C8DEE8", - "spec": { - "space": "srgb", - "hex": "#c8dee8" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "433a07b5657b5421", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#b0d0de", - "raw": "#B0D0DE", - "spec": { - "space": "srgb", - "hex": "#b0d0de" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "8498451922cf9169", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#5a8a9c", - "raw": "#5A8A9C", - "spec": { - "space": "srgb", - "hex": "#5a8a9c" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "cbe9f08f6c144189", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3a6a7c", - "raw": "#3A6A7C", - "spec": { - "space": "srgb", - "hex": "#3a6a7c" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "d03c74ec877b8311", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3ab4d8", - "raw": "#3AB4D8", - "spec": { - "space": "srgb", - "hex": "#3ab4d8" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "a9867ee562d713ea", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#2e9ab8", - "raw": "#2E9AB8", - "spec": { - "space": "srgb", - "hex": "#2e9ab8" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "78ec743cafe21123", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#5b78d8", - "raw": "#5B78D8", - "spec": { - "space": "srgb", - "hex": "#5b78d8" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "36b412ad467c600d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e07a5f", - "raw": "#E07A5F", - "spec": { - "space": "srgb", - "hex": "#e07a5f" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "361fd5cfe610997f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7bc4b8", - "raw": "#7BC4B8", - "spec": { - "space": "srgb", - "hex": "#7bc4b8" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "105b5920a528b770", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a8c256", - "raw": "#A8C256", - "spec": { - "space": "srgb", - "hex": "#a8c256" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "bbf7b29fe5fc3c87", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#0a1e28", - "raw": "#0A1E28", - "spec": { - "space": "srgb", - "hex": "#0a1e28" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "808e54de5417e5f1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#112a36", - "raw": "#112A36", - "spec": { - "space": "srgb", - "hex": "#112a36" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "b5f58abf67dc4697", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#2a5060", - "raw": "#2A5060", - "spec": { - "space": "srgb", - "hex": "#2a5060" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "f45fb001affa22e2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#1a3a4a", - "raw": "#1A3A4A", - "spec": { - "space": "srgb", - "hex": "#1a3a4a" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "457034b7fd46c64f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3a6070", - "raw": "#3A6070", - "spec": { - "space": "srgb", - "hex": "#3a6070" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "676c0ff352510834", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7b92e8", - "raw": "#7B92E8", - "spec": { - "space": "srgb", - "hex": "#7b92e8" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "b1c81cbbea164b5b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c5a24d", - "raw": "#C5A24D", - "spec": { - "space": "srgb", - "hex": "#c5a24d" - }, - "occurrences": 9, - "files_count": 1 - }, - { - "id": "b2ab67f5ea9ddb0e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f8f6f9", - "raw": "#F8F6F9", - "spec": { - "space": "srgb", - "hex": "#f8f6f9" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "94d7f6edc861edfc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#1e1528", - "raw": "#1E1528", - "spec": { - "space": "srgb", - "hex": "#1e1528" - }, - "occurrences": 8, - "files_count": 1 - }, - { - "id": "1078e14eeebcb695", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ede8f2", - "raw": "#EDE8F2", - "spec": { - "space": "srgb", - "hex": "#ede8f2" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "4036630e68804cb2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f0ecf4", - "raw": "#F0ECF4", - "spec": { - "space": "srgb", - "hex": "#f0ecf4" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "668664693fc5bf1c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#b8a8c8", - "raw": "#B8A8C8", - "spec": { - "space": "srgb", - "hex": "#b8a8c8" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "19d2cf57101a62bb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ddd4e6", - "raw": "#DDD4E6", - "spec": { - "space": "srgb", - "hex": "#ddd4e6" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "c2253a13a7fcddac", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ccc0d8", - "raw": "#CCC0D8", - "spec": { - "space": "srgb", - "hex": "#ccc0d8" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "79bf1c7e1b57227d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7a6b8a", - "raw": "#7A6B8A", - "spec": { - "space": "srgb", - "hex": "#7a6b8a" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "59f91e5bbf018524", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#5a4b6a", - "raw": "#5A4B6A", - "spec": { - "space": "srgb", - "hex": "#5a4b6a" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "92db03f2c3a281d7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#8b6cc1", - "raw": "#8B6CC1", - "spec": { - "space": "srgb", - "hex": "#8b6cc1" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "83d212cbc9d97b6d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#5e8ec5", - "raw": "#5E8EC5", - "spec": { - "space": "srgb", - "hex": "#5e8ec5" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "8db60e3f22e4f6d2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7cb88a", - "raw": "#7CB88A", - "spec": { - "space": "srgb", - "hex": "#7cb88a" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "9fd15a2afca45ce1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#0e0a14", - "raw": "#0E0A14", - "spec": { - "space": "srgb", - "hex": "#0e0a14" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "a7a325145c69a5c6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#1a1224", - "raw": "#1A1224", - "spec": { - "space": "srgb", - "hex": "#1a1224" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "0465f321cc84d071", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3a2a50", - "raw": "#3A2A50", - "spec": { - "space": "srgb", - "hex": "#3a2a50" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "74891c7365ff5422", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#2a1e3a", - "raw": "#2A1E3A", - "spec": { - "space": "srgb", - "hex": "#2a1e3a" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "883207b6dede4313", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#4a3a60", - "raw": "#4A3A60", - "spec": { - "space": "srgb", - "hex": "#4a3a60" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "d43ecbdaea31827e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a88de0", - "raw": "#A88DE0", - "spec": { - "space": "srgb", - "hex": "#a88de0" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "443e7ce11699d14e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e08090", - "raw": "#E08090", - "spec": { - "space": "srgb", - "hex": "#e08090" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "759883dd09050d8f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7aaee5", - "raw": "#7AAEE5", - "spec": { - "space": "srgb", - "hex": "#7aaee5" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "3050fd951662c609", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#8bd09a", - "raw": "#8BD09A", - "spec": { - "space": "srgb", - "hex": "#8bd09a" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "96415a002e6b5f8e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ff3a00", - "raw": "#FF3A00", - "spec": { - "space": "srgb", - "hex": "#ff3a00" - }, - "occurrences": 6, - "files_count": 1 - }, - { - "id": "966ee150a7ef1f62", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ff5722", - "raw": "#FF5722", - "spec": { - "space": "srgb", - "hex": "#ff5722" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "b35e709f2b675d2e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f0f0f0", - "raw": "#F0F0F0", - "spec": { - "space": "srgb", - "hex": "#f0f0f0" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "d173af0295988913", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#cccccc", - "raw": "#CCCCCC", - "spec": { - "space": "srgb", - "hex": "#cccccc" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "98c5014352173c56", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#555555", - "raw": "#555555", - "spec": { - "space": "srgb", - "hex": "#555555" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "d9ec20e4ced88fe8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#0a0a0a", - "raw": "#0A0A0A", - "spec": { - "space": "srgb", - "hex": "#0a0a0a" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "17b19b35c81a2f65", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#141414", - "raw": "#141414", - "spec": { - "space": "srgb", - "hex": "#141414" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "33476384dd13580c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#444444", - "raw": "#444444", - "spec": { - "space": "srgb", - "hex": "#444444" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "15de92119aba4e00", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#aaaaaa", - "raw": "#AAAAAA", - "spec": { - "space": "srgb", - "hex": "#aaaaaa" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "e4e3d2d0bf2c562a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#00e5ff", - "raw": "#00E5FF", - "spec": { - "space": "srgb", - "hex": "#00e5ff" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "8ff64297d2b2e1a0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ffe600", - "raw": "#FFE600", - "spec": { - "space": "srgb", - "hex": "#ffe600" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "9d0a8b9a57dc4978", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#ff00aa", - "raw": "#FF00AA", - "spec": { - "space": "srgb", - "hex": "#ff00aa" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "28012a7281f0c2fc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#00ff66", - "raw": "#00FF66", - "spec": { - "space": "srgb", - "hex": "#00ff66" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "d64d387f31870e1b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#8b7ec8", - "raw": "#8B7EC8", - "spec": { - "space": "srgb", - "hex": "#8b7ec8" - }, - "occurrences": 4, - "files_count": 1 - }, - { - "id": "3fffa01086eb6297", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#fafafa", - "raw": "#FAFAFA", - "spec": { - "space": "srgb", - "hex": "#fafafa" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "e7bd8fc856e49638", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3a3552", - "raw": "#3A3552", - "spec": { - "space": "srgb", - "hex": "#3a3552" - }, - "occurrences": 5, - "files_count": 1 - }, - { - "id": "17c62c983025f368", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f5f2fa", - "raw": "#F5F2FA", - "spec": { - "space": "srgb", - "hex": "#f5f2fa" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "08899c20ea9a44fb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c8bee0", - "raw": "#C8BEE0", - "spec": { - "space": "srgb", - "hex": "#c8bee0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "0f0c5543ebbf6f22", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#f0edf8", - "raw": "#F0EDF8", - "spec": { - "space": "srgb", - "hex": "#f0edf8" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "adf12185f8bb58f5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e0daf0", - "raw": "#E0DAF0", - "spec": { - "space": "srgb", - "hex": "#e0daf0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "ca7566df03a19c79", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d2cae4", - "raw": "#D2CAE4", - "spec": { - "space": "srgb", - "hex": "#d2cae4" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "758e24e80a43ccfc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#8a82a0", - "raw": "#8A82A0", - "spec": { - "space": "srgb", - "hex": "#8a82a0" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "b0131f9e5c49df05", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#6a6280", - "raw": "#6A6280", - "spec": { - "space": "srgb", - "hex": "#6a6280" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "b615dcc072f85de6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e8a0a0", - "raw": "#E8A0A0", - "spec": { - "space": "srgb", - "hex": "#e8a0a0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "e98f26ecbcb7b6c0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a0d8a0", - "raw": "#A0D8A0", - "spec": { - "space": "srgb", - "hex": "#a0d8a0" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "6569450039479881", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a0c0e8", - "raw": "#A0C0E8", - "spec": { - "space": "srgb", - "hex": "#a0c0e8" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "8c7f4323fff851f6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e8d0a0", - "raw": "#E8D0A0", - "spec": { - "space": "srgb", - "hex": "#e8d0a0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "25194db5d08acd2d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c07070", - "raw": "#C07070", - "spec": { - "space": "srgb", - "hex": "#c07070" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "d779d7cb37f29253", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#70a870", - "raw": "#70A870", - "spec": { - "space": "srgb", - "hex": "#70a870" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "1377e85c660d5e72", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c0a060", - "raw": "#C0A060", - "spec": { - "space": "srgb", - "hex": "#c0a060" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "f30049a4156df82a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#7090c0", - "raw": "#7090C0", - "spec": { - "space": "srgb", - "hex": "#7090c0" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "70b8c2895dbb7f8c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#c8a8e0", - "raw": "#C8A8E0", - "spec": { - "space": "srgb", - "hex": "#c8a8e0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "68dbc4183af3b47b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a8c8e8", - "raw": "#A8C8E8", - "spec": { - "space": "srgb", - "hex": "#a8c8e8" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "f3f71f08ccce273c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e8b0b0", - "raw": "#E8B0B0", - "spec": { - "space": "srgb", - "hex": "#e8b0b0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "15096680f945c96d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#b0d8b0", - "raw": "#B0D8B0", - "spec": { - "space": "srgb", - "hex": "#b0d8b0" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "060150e4dae9dc87", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e8d0a8", - "raw": "#E8D0A8", - "spec": { - "space": "srgb", - "hex": "#e8d0a8" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "ec78ecf373ce6a31", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#14121c", - "raw": "#14121C", - "spec": { - "space": "srgb", - "hex": "#14121c" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "6ef2c7cb6f43f5f2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#1e1a2a", - "raw": "#1E1A2A", - "spec": { - "space": "srgb", - "hex": "#1e1a2a" - }, - "occurrences": 2, - "files_count": 1 - }, - { - "id": "b6912c683ca458e7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#3a3452", - "raw": "#3A3452", - "spec": { - "space": "srgb", - "hex": "#3a3452" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "6092838e4cc3a56f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#2a2640", - "raw": "#2A2640", - "spec": { - "space": "srgb", - "hex": "#2a2640" - }, - "occurrences": 3, - "files_count": 1 - }, - { - "id": "c17a154eb84c2b6e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#4a4468", - "raw": "#4A4468", - "spec": { - "space": "srgb", - "hex": "#4a4468" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "6e9bc95701aee50e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#a898e0", - "raw": "#A898E0", - "spec": { - "space": "srgb", - "hex": "#a898e0" - }, - "occurrences": 4, - "files_count": 1 - }, - { - "id": "00a943aa1d8c5f05", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e8e4f4", - "raw": "#E8E4F4", - "spec": { - "space": "srgb", - "hex": "#e8e4f4" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "7e9a0e7a52bfd561", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d08080", - "raw": "#D08080", - "spec": { - "space": "srgb", - "hex": "#d08080" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "79da8c8539dfd8b0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#80c080", - "raw": "#80C080", - "spec": { - "space": "srgb", - "hex": "#80c080" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "c146f8d19e0000c1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#80a8d8", - "raw": "#80A8D8", - "spec": { - "space": "srgb", - "hex": "#80a8d8" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "f88d732d9098b718", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d8c080", - "raw": "#D8C080", - "spec": { - "space": "srgb", - "hex": "#d8c080" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "d513e3c883fcce68", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e0a0a0", - "raw": "#E0A0A0", - "spec": { - "space": "srgb", - "hex": "#e0a0a0" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "eac4a1ff37189b53", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#e0c880", - "raw": "#E0C880", - "spec": { - "space": "srgb", - "hex": "#e0c880" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "3bf472ce0b0a4d51", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "#d76a7a", - "raw": "#D76A7A", - "spec": { - "space": "srgb", - "hex": "#d76a7a" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "1ae232a2b1051333", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(26,26,26,0.1)", - "raw": "rgba(26, 26, 26, 0.1)", - "spec": { - "space": "srgb", - "raw": "rgba(26, 26, 26, 0.1)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "5b18fd97ee008087", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(26,26,26,0.4)", - "raw": "rgba(26, 26, 26, 0.4)", - "spec": { - "space": "srgb", - "raw": "rgba(26, 26, 26, 0.4)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "7f45d37288e9c2d1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(26,26,26,0.04)", - "raw": "rgba(26, 26, 26, 0.04)", - "spec": { - "space": "srgb", - "raw": "rgba(26, 26, 26, 0.04)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "19a94703e5d71f48", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(26,26,26,0.15)", - "raw": "rgba(26, 26, 26, 0.15)", - "spec": { - "space": "srgb", - "raw": "rgba(26, 26, 26, 0.15)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "faba31a4f3e1387a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(242,242,242,0.1)", - "raw": "rgba(242, 242, 242, 0.1)", - "spec": { - "space": "srgb", - "raw": "rgba(242, 242, 242, 0.1)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "28ec14c0cbedd96b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(242,242,242,0.4)", - "raw": "rgba(242, 242, 242, 0.4)", - "spec": { - "space": "srgb", - "raw": "rgba(242, 242, 242, 0.4)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "c8981164f37ccec8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(242,242,242,0.04)", - "raw": "rgba(242, 242, 242, 0.04)", - "spec": { - "space": "srgb", - "raw": "rgba(242, 242, 242, 0.04)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "5062d98b30967acc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(244,244,245,0.1)", - "raw": "rgba(244, 244, 245, 0.1)", - "spec": { - "space": "srgb", - "raw": "rgba(244, 244, 245, 0.1)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "70b35b09bbecf6b6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(255,255,255,0.5)", - "raw": "rgba(255, 255, 255, 0.5)", - "spec": { - "space": "srgb", - "raw": "rgba(255, 255, 255, 0.5)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "6e6313ba86d28845", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(255,255,255,0.08)", - "raw": "rgba(255, 255, 255, 0.08)", - "spec": { - "space": "srgb", - "raw": "rgba(255, 255, 255, 0.08)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "571ae129eda92472", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(76,76,76,0.15)", - "raw": "rgba(76, 76, 76, 0.15)", - "spec": { - "space": "srgb", - "raw": "rgba(76, 76, 76, 0.15)" - }, - "occurrences": 4, - "files_count": 2 - }, - { - "id": "b79385abe9c9da26", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(76,76,76,0.1)", - "raw": "rgba(76, 76, 76, 0.1)", - "spec": { - "space": "srgb", - "raw": "rgba(76, 76, 76, 0.1)" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "5cfc6f84f1afe19c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(76,76,76,0.22)", - "raw": "rgba(76, 76, 76, 0.22)", - "spec": { - "space": "srgb", - "raw": "rgba(76, 76, 76, 0.22)" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "15cf208c50c812f7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(0,0,0,0.12)", - "raw": "rgba(0, 0, 0, 0.12)", - "spec": { - "space": "srgb", - "raw": "rgba(0, 0, 0, 0.12)" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "1abc25e240f866d8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(0,0,0,0.2)", - "raw": "rgba(0, 0, 0, 0.2)", - "spec": { - "space": "srgb", - "raw": "rgba(0, 0, 0, 0.2)" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "987105b73f46eac4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(0,0,0,0.3)", - "raw": "rgba(0, 0, 0, 0.3)", - "spec": { - "space": "srgb", - "raw": "rgba(0, 0, 0, 0.3)" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "322c1a76c47c99a2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(0,0,0,0.4)", - "raw": "rgba(0, 0, 0, 0.4)", - "spec": { - "space": "srgb", - "raw": "rgba(0, 0, 0, 0.4)" - }, - "occurrences": 4, - "files_count": 2 - }, - { - "id": "3bf722662b86ab9b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(0,0,0,0.5)", - "raw": "rgba(0, 0, 0, 0.5)", - "spec": { - "space": "srgb", - "raw": "rgba(0, 0, 0, 0.5)" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "9a2a76757824851d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "color", - "value": "rgba(0,0,0,0.6)", - "raw": "rgba(0, 0, 0, 0.6)", - "spec": { - "space": "srgb", - "raw": "rgba(0, 0, 0, 0.6)" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "27e8f5e18050c0c6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "0px", - "raw": "0", - "spec": { - "scalar": 0, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "6b694ae3c65ed348", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "10px", - "raw": "--radius-dropdown", - "spec": { - "scalar": 10, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "38aff7490f8e911a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "14px", - "raw": "--radius-card-sm", - "spec": { - "scalar": 14, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "264664c4e93ae355", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "16px", - "raw": "--radius-modal", - "spec": { - "scalar": 16, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "6eec29f3464c2715", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "20px", - "raw": "--radius / --page-container-side-gutter", - "spec": { - "scalar": 20, - "unit": "px" - }, - "occurrences": 2, - "files_count": 1, - "usage": { - "css_var": 2 - } - }, - { - "id": "95f29297845963cf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "24px", - "raw": "--radius-card-lg", - "spec": { - "scalar": 24, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "ac1e835531a8fca6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "75px", - "raw": "--section-heading-margin-bottom", - "spec": { - "scalar": 75, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "7fa5b999d3ee4aef", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "100px", - "raw": "--section-padding-vertical", - "spec": { - "scalar": 100, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "24316dd8a9561c61", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "1440px", - "raw": "--page-container-max-width", - "spec": { - "scalar": 1440, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "b3293c9677e7f813", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "999px", - "raw": "--radius-pill / --radius-button / --radius-input / scrollbar", - "spec": { - "scalar": 999, - "unit": "px" - }, - "occurrences": 4, - "files_count": 2, - "usage": { - "css_var": 4 - } - }, - { - "id": "78217f319c50857e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "6px", - "raw": "scrollbar width/height", - "spec": { - "scalar": 6, - "unit": "px" - }, - "occurrences": 2, - "files_count": 1, - "usage": { - "css_var": 2 - } - }, - { - "id": "a69b2cd03e7912b4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "44px", - "raw": "--spacing-input-sm / --spacing-button (2.75rem)", - "spec": { - "scalar": 44, - "unit": "px" - }, - "occurrences": 2, - "files_count": 1, - "usage": { - "css_var": 2 - } - }, - { - "id": "36650f3788982b64", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "52px", - "raw": "--spacing-input (3.25rem)", - "spec": { - "scalar": 52, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "fcc1f4c4b95b9648", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "32px", - "raw": "--spacing-button-sm (2rem)", - "spec": { - "scalar": 32, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "40cfc771f88adce8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "0px-tw", - "raw": "tw p-0/m-0/gap-0", - "spec": { - "scalar": 0, - "unit": "px" - }, - "occurrences": 24, - "files_count": 9, - "usage": { - "className": 24 - } - }, - { - "id": "90ba5a7b2659cf76", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "2px-tw", - "raw": "tw 0.5", - "spec": { - "scalar": 2, - "unit": "px" - }, - "occurrences": 7, - "files_count": 5, - "usage": { - "className": 7 - } - }, - { - "id": "1823dbf90651719f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "4px-tw", - "raw": "tw 1", - "spec": { - "scalar": 4, - "unit": "px" - }, - "occurrences": 17, - "files_count": 12, - "usage": { - "className": 17 - } - }, - { - "id": "37d82a6b3fe6bbf1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "6px-tw", - "raw": "tw 1.5", - "spec": { - "scalar": 6, - "unit": "px" - }, - "occurrences": 8, - "files_count": 6, - "usage": { - "className": 8 - } - }, - { - "id": "f6885e3ad678418f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "8px-tw", - "raw": "tw 2", - "spec": { - "scalar": 8, - "unit": "px" - }, - "occurrences": 60, - "files_count": 30, - "usage": { - "className": 60 - } - }, - { - "id": "8fb832a0e24354b7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "10px-tw", - "raw": "tw 2.5", - "spec": { - "scalar": 10, - "unit": "px" - }, - "occurrences": 6, - "files_count": 5, - "usage": { - "className": 6 - } - }, - { - "id": "ce95f93285fdcde3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "12px-tw", - "raw": "tw 3", - "spec": { - "scalar": 12, - "unit": "px" - }, - "occurrences": 30, - "files_count": 20, - "usage": { - "className": 30 - } - }, - { - "id": "a4323cc68b5c7b61", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "14px-tw", - "raw": "tw 3.5", - "spec": { - "scalar": 14, - "unit": "px" - }, - "occurrences": 2, - "files_count": 2, - "usage": { - "className": 2 - } - }, - { - "id": "48b1bd0526aa4045", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "16px-tw", - "raw": "tw 4", - "spec": { - "scalar": 16, - "unit": "px" - }, - "occurrences": 25, - "files_count": 18, - "usage": { - "className": 25 - } - }, - { - "id": "af7881e477856e59", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "20px-tw", - "raw": "tw 5", - "spec": { - "scalar": 20, - "unit": "px" - }, - "occurrences": 6, - "files_count": 4, - "usage": { - "className": 6 - } - }, - { - "id": "e5041291baee6312", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "24px-tw", - "raw": "tw 6", - "spec": { - "scalar": 24, - "unit": "px" - }, - "occurrences": 8, - "files_count": 6, - "usage": { - "className": 8 - } - }, - { - "id": "4cd65b8f780d949c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "32px-tw", - "raw": "tw 8", - "spec": { - "scalar": 32, - "unit": "px" - }, - "occurrences": 8, - "files_count": 6, - "usage": { - "className": 8 - } - }, - { - "id": "479d5c09a02ca680", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "36px-tw", - "raw": "tw 9", - "spec": { - "scalar": 36, - "unit": "px" - }, - "occurrences": 4, - "files_count": 4, - "usage": { - "className": 4 - } - }, - { - "id": "57c285eb6d534f19", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "40px-tw", - "raw": "tw 10", - "spec": { - "scalar": 40, - "unit": "px" - }, - "occurrences": 4, - "files_count": 3, - "usage": { - "className": 4 - } - }, - { - "id": "89a649986113def8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "spacing", - "value": "48px-tw", - "raw": "tw 12", - "spec": { - "scalar": 48, - "unit": "px" - }, - "occurrences": 3, - "files_count": 3, - "usage": { - "className": 3 - } - }, - { - "id": "52d53beb11a01b4c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "10px", - "raw": "--text-xxs", - "spec": { - "size": { - "scalar": 10, - "unit": "px" - } - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "b7fb48a1f3cab84c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "11px", - "raw": "--label-font-size", - "spec": { - "size": { - "scalar": 11, - "unit": "px" - }, - "weight": 600, - "line_height": 1.2, - "letter_spacing": { - "scalar": 0.12, - "unit": "em" - } - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "16d4d904707b4644", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "heading-display", - "raw": "--heading-display-font-size: clamp(64px, 8vw, 96px)", - "spec": { - "size": { - "scalar": 96, - "unit": "px" - }, - "weight": 900, - "line_height": 0.88, - "letter_spacing": { - "scalar": -0.05, - "unit": "em" - }, - "raw": "clamp(64px, 8vw, 96px)" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "0e906bc1b03aa644", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "heading-section", - "raw": "--heading-section-font-size: clamp(44px, 5vw, 64px)", - "spec": { - "size": { - "scalar": 64, - "unit": "px" - }, - "weight": 700, - "line_height": 0.95, - "letter_spacing": { - "scalar": -0.035, - "unit": "em" - }, - "raw": "clamp(44px, 5vw, 64px)" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "160035f840b0c5d6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "heading-sub", - "raw": "--heading-sub-font-size: clamp(28px, 3vw, 40px)", - "spec": { - "size": { - "scalar": 40, - "unit": "px" - }, - "weight": 700, - "line_height": 1, - "letter_spacing": { - "scalar": -0.02, - "unit": "em" - }, - "raw": "clamp(28px, 3vw, 40px)" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "571462263eb493ce", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "heading-card", - "raw": "--heading-card-font-size: clamp(20px, 2vw, 28px)", - "spec": { - "size": { - "scalar": 28, - "unit": "px" - }, - "weight": 600, - "line_height": 1.1, - "letter_spacing": { - "scalar": -0.01, - "unit": "em" - }, - "raw": "clamp(20px, 2vw, 28px)" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "b771f26d772b25bc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "display", - "raw": "--display-size: clamp(3rem, 12vw, 12rem)", - "spec": { - "size": { - "scalar": 192, - "unit": "px" - }, - "line_height": 0.85, - "letter_spacing": { - "scalar": -0.05, - "unit": "em" - }, - "raw": "clamp(3rem, 12vw, 12rem)" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "0cf477428105a24b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "body-reading", - "raw": "--body-reading-size: clamp(1rem, 1.3vw, 1.25rem)", - "spec": { - "size": { - "scalar": 20, - "unit": "px" - }, - "line_height": 1.65, - "letter_spacing": { - "scalar": -0.01, - "unit": "em" - }, - "raw": "clamp(1rem, 1.3vw, 1.25rem)" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "5a368e8b0da7c187", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "pullquote", - "raw": "--pullquote-size: clamp(1.5rem, 3vw, 2.5rem)", - "spec": { - "size": { - "scalar": 40, - "unit": "px" - }, - "weight": 300, - "line_height": 1.3, - "letter_spacing": { - "scalar": -0.02, - "unit": "em" - }, - "raw": "clamp(1.5rem, 3vw, 2.5rem)" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "9fed5e386e0ca49c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "12px-tw", - "raw": "text-xs", - "spec": { - "size": { - "scalar": 12, - "unit": "px" - } - }, - "occurrences": 80, - "files_count": 33, - "usage": { - "className": 80 - } - }, - { - "id": "7fec57454e3fd35b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "14px-tw", - "raw": "text-sm", - "spec": { - "size": { - "scalar": 14, - "unit": "px" - } - }, - "occurrences": 99, - "files_count": 38, - "usage": { - "className": 99 - } - }, - { - "id": "3006f2a8b1e17ea7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "16px-tw", - "raw": "text-base", - "spec": { - "size": { - "scalar": 16, - "unit": "px" - } - }, - "occurrences": 2, - "files_count": 2, - "usage": { - "className": 2 - } - }, - { - "id": "210a1d8ce95b1c9e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "18px-tw", - "raw": "text-lg", - "spec": { - "size": { - "scalar": 18, - "unit": "px" - } - }, - "occurrences": 2, - "files_count": 2, - "usage": { - "className": 2 - } - }, - { - "id": "ec714f895ec9e6e2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "font-sans", - "raw": "--font-sans", - "spec": { - "family": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "a68134534bd1b561", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "font-mono", - "raw": "--font-mono", - "spec": { - "family": "'Geist Mono', monospace" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "a71f8e59e5e5c70d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "font-serif", - "raw": "--font-serif", - "spec": { - "family": "serif" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "8c8e09990da98518", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "typography", - "value": "font-display", - "raw": "--font-display", - "spec": { - "family": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "559a124c899ad1d7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "0px", - "raw": "neon-brutalist override --radius/--radius-pill/etc: 0px", - "spec": { - "scalar": 0, - "unit": "px" - }, - "occurrences": 9, - "files_count": 1, - "usage": { - "css_var": 9 - } - }, - { - "id": "ab95d0ac1c96c16f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "10px", - "raw": "--radius-dropdown", - "spec": { - "scalar": 10, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "b6f58b068cfba43d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "14px", - "raw": "--radius-card-sm", - "spec": { - "scalar": 14, - "unit": "px" - }, - "occurrences": 2, - "files_count": 2, - "usage": { - "css_var": 2 - } - }, - { - "id": "d3799a457d122a98", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "16px", - "raw": "--radius-modal / --radius-sm", - "spec": { - "scalar": 16, - "unit": "px" - }, - "occurrences": 3, - "files_count": 2, - "usage": { - "css_var": 3 - } - }, - { - "id": "9bc3965875814101", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "18px", - "raw": "--radius-md (calc(var(--radius)-2))", - "spec": { - "scalar": 18, - "unit": "px" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "b887c814343d7f6e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "20px", - "raw": "--radius / --radius-card / --radius-lg", - "spec": { - "scalar": 20, - "unit": "px" - }, - "occurrences": 4, - "files_count": 2, - "usage": { - "css_var": 4 - } - }, - { - "id": "3944c10d1f2c2889", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "24px", - "raw": "--radius-card-lg / --radius-xl", - "spec": { - "scalar": 24, - "unit": "px" - }, - "occurrences": 3, - "files_count": 2, - "usage": { - "css_var": 3 - } - }, - { - "id": "6151c6fbb1a9fe3c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "radius", - "value": "999px", - "raw": "--radius-pill / --radius-button / --radius-input", - "spec": { - "scalar": 999, - "unit": "px" - }, - "occurrences": 3, - "files_count": 1, - "usage": { - "css_var": 3 - } - }, - { - "id": "fd05aa049babb3c0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-mini-light", - "raw": "0 2px 8px rgba(76, 76, 76, 0.15)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(76, 76, 76, 0.15)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "2dcd93c75a34b409", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-mini-inset-light", - "raw": "0 1px 4px rgba(76, 76, 76, 0.1) inset", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 1, - "unit": "px" - }, - "blur": { - "scalar": 4, - "unit": "px" - }, - "color": "rgba(76, 76, 76, 0.1)", - "inset": true, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "662e5379b411423d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-btn-light", - "raw": "0 2px 8px rgba(76, 76, 76, 0.15)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(76, 76, 76, 0.15)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "c0397c67e2556659", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-card-light", - "raw": "0 2px 8px rgba(76, 76, 76, 0.15)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(76, 76, 76, 0.15)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "3b98bce0266142f0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-elevated-light", - "raw": "0 3px 12px rgba(76, 76, 76, 0.22)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 3, - "unit": "px" - }, - "blur": { - "scalar": 12, - "unit": "px" - }, - "color": "rgba(76, 76, 76, 0.22)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "e93b43c6368c1c28", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-popover-light", - "raw": "0 8px 30px rgba(0, 0, 0, 0.12)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 8, - "unit": "px" - }, - "blur": { - "scalar": 30, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.12)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "0d139cb5380092d5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-modal-light", - "raw": "0 20px 60px rgba(0, 0, 0, 0.2)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 20, - "unit": "px" - }, - "blur": { - "scalar": 60, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.2)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "3ac1e882447f6407", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-kbd-light", - "raw": "0 2px 8px rgba(76, 76, 76, 0.15)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(76, 76, 76, 0.15)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "4a0f3b63f798cd2b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-date-field-focus-light", - "raw": "0 0 0 3px rgba(26, 26, 26, 0.15)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 0, - "unit": "px" - }, - "blur": { - "scalar": 0, - "unit": "px" - }, - "spread": { - "scalar": 3, - "unit": "px" - }, - "color": "rgba(26, 26, 26, 0.15)", - "inset": false, - "theme": "light" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "4ed47452db34d249", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-mini-dark", - "raw": "0 2px 8px rgba(0, 0, 0, 0.4)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.4)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "9a1e411637f9298b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-mini-inset-dark", - "raw": "0 1px 4px rgba(0, 0, 0, 0.5) inset", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 1, - "unit": "px" - }, - "blur": { - "scalar": 4, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.5)", - "inset": true, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "6176737b2c3c5b1e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-btn-dark", - "raw": "0 2px 8px rgba(0, 0, 0, 0.3)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.3)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "ecd15c84fa5f72f0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-card-dark", - "raw": "0 2px 8px rgba(0, 0, 0, 0.4)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.4)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "c75cab37c31c8b9f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-elevated-dark", - "raw": "0 3px 12px rgba(0, 0, 0, 0.5)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 3, - "unit": "px" - }, - "blur": { - "scalar": 12, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.5)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "1c505fb58c99a293", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-popover-dark", - "raw": "0 8px 30px rgba(0, 0, 0, 0.4)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 8, - "unit": "px" - }, - "blur": { - "scalar": 30, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.4)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "287f698a328618f0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-modal-dark", - "raw": "0 20px 60px rgba(0, 0, 0, 0.6)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 20, - "unit": "px" - }, - "blur": { - "scalar": 60, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.6)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "ed2e277f24ba23a1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-kbd-dark", - "raw": "0 2px 8px rgba(0, 0, 0, 0.4)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 2, - "unit": "px" - }, - "blur": { - "scalar": 8, - "unit": "px" - }, - "color": "rgba(0, 0, 0, 0.4)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "10047fe335386c60", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "shadow", - "value": "shadow-date-field-focus-dark", - "raw": "0 0 0 3px rgba(244, 244, 245, 0.1)", - "spec": { - "offset_x": { - "scalar": 0, - "unit": "px" - }, - "offset_y": { - "scalar": 0, - "unit": "px" - }, - "blur": { - "scalar": 0, - "unit": "px" - }, - "spread": { - "scalar": 3, - "unit": "px" - }, - "color": "rgba(244, 244, 245, 0.1)", - "inset": false, - "theme": "dark" - }, - "occurrences": 1, - "files_count": 2 - }, - { - "id": "b3ddca1c9bc8beb0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "accordion-down", - "raw": "accordion-down 0.2s ease-out", - "spec": { - "duration_ms": 200, - "easing": "ease-out" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "e19ce7adf698f0ba", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "accordion-up", - "raw": "accordion-up 0.2s ease-out", - "spec": { - "duration_ms": 200, - "easing": "ease-out" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "14a31264e662e2a4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "caret-blink", - "raw": "caret-blink 1s ease-out infinite", - "spec": { - "duration_ms": 1000, - "easing": "ease-out", - "iteration": "infinite" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "a7e5543238bc1d97", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "scale-in", - "raw": "scale-in 0.2s ease", - "spec": { - "duration_ms": 200, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "24f92a19199d493c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "scale-out", - "raw": "scale-out 0.15s ease", - "spec": { - "duration_ms": 150, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "a99926a35e1e9365", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "fade-in", - "raw": "fade-in 0.2s ease", - "spec": { - "duration_ms": 200, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "c2b6002ad679aa39", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "fade-out", - "raw": "fade-out 0.15s ease", - "spec": { - "duration_ms": 150, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "49ea7d48ae3ac5ec", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "enter-from-left", - "raw": "enter-from-left 0.2s ease", - "spec": { - "duration_ms": 200, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "31c6b552147f47f0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "enter-from-right", - "raw": "enter-from-right 0.2s ease", - "spec": { - "duration_ms": 200, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "03aff8d1793f2cde", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "exit-to-left", - "raw": "exit-to-left 0.2s ease", - "spec": { - "duration_ms": 200, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "95ee17e0ef6a7b76", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "exit-to-right", - "raw": "exit-to-right 0.2s ease", - "spec": { - "duration_ms": 200, - "easing": "ease" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "8d15f1098018dc56", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "word-reveal", - "raw": "word-reveal 0.4s ease-out", - "spec": { - "duration_ms": 400, - "easing": "ease-out" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "f45c59a229828298", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "fadeToFull", - "raw": "@keyframes fadeToFull", - "spec": { - "duration_ms": null, - "easing": null - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "2849dd656ffbed56", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "fadeToSubtle", - "raw": "@keyframes fadeToSubtle", - "spec": { - "duration_ms": null, - "easing": null - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "3e99ff54b9a42162", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "hero-blur-in", - "raw": "@keyframes hero-blur-in", - "spec": { - "duration_ms": null, - "easing": null - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "c6c2e879c9a9276e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "duration-fast", - "raw": "--duration-fast: 0.15s", - "spec": { - "duration_ms": 150 - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "8a5b3bdd48cdc41d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "duration-normal", - "raw": "--duration-normal: 0.2s", - "spec": { - "duration_ms": 200 - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "218eaaa5b0c71dfb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "duration-slow", - "raw": "--duration-slow: 0.4s", - "spec": { - "duration_ms": 400 - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "f4961a1f347591eb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "motion", - "value": "ease-spring", - "raw": "--ease-spring: cubic-bezier(0.33, 1, 0.68, 1)", - "spec": { - "easing": "cubic-bezier(0.33, 1, 0.68, 1)" - }, - "occurrences": 1, - "files_count": 1 - }, - { - "id": "49e630dae7506fe3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "breakpoint", - "value": "desktop", - "raw": "--breakpoint-desktop: 1440px", - "spec": { - "scalar": 1440, - "unit": "px", - "label": "desktop" - }, - "occurrences": 1, - "files_count": 1, - "usage": { - "css_var": 1 - } - }, - { - "id": "9007dff208008af0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "breakpoint", - "value": "sm", - "raw": "tailwind sm: prefix (default 640px)", - "spec": { - "scalar": 640, - "unit": "px", - "label": "sm" - }, - "occurrences": 21, - "files_count": 8, - "usage": { - "className": 21 - } - }, - { - "id": "e54aec7447e542c9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "breakpoint", - "value": "md", - "raw": "tailwind md: prefix (default 768px)", - "spec": { - "scalar": 768, - "unit": "px", - "label": "md" - }, - "occurrences": 14, - "files_count": 8, - "usage": { - "className": 14 - } - }, - { - "id": "56b71cd8b30680cd", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "kind": "breakpoint", - "value": "lg", - "raw": "tailwind lg: prefix (default 1024px)", - "spec": { - "scalar": 1024, - "unit": "px", - "label": "lg" - }, - "occurrences": 3, - "files_count": 2, - "usage": { - "className": 3 - } - } - ], - "tokens": [ - { - "id": "b852ac0d2a4d3a5c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-white", - "alias_chain": [], - "resolved_value": "#ffffff", - "occurrences": 1 - }, - { - "id": "9307d5b28ee91a00", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-black", - "alias_chain": [], - "resolved_value": "#000000", - "occurrences": 1 - }, - { - "id": "dffd919eea5602a4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-50", - "alias_chain": [], - "resolved_value": "#f5f5f5", - "occurrences": 1 - }, - { - "id": "b6034809ce1efed1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-100", - "alias_chain": [], - "resolved_value": "#f0f0f0", - "occurrences": 1 - }, - { - "id": "d61258a047bdf555", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-200", - "alias_chain": [], - "resolved_value": "#e8e8e8", - "occurrences": 1 - }, - { - "id": "769863fc032d8a6c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-300", - "alias_chain": [], - "resolved_value": "#e5e5e5", - "occurrences": 1 - }, - { - "id": "ea096cd6c213fc16", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-400", - "alias_chain": [], - "resolved_value": "#cccccc", - "occurrences": 1 - }, - { - "id": "16a443a7c3fb42c2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-500", - "alias_chain": [], - "resolved_value": "#999999", - "occurrences": 1 - }, - { - "id": "c778a4448acbcbe2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-600", - "alias_chain": [], - "resolved_value": "#666666", - "occurrences": 1 - }, - { - "id": "4c76fca6a9613f8b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-700", - "alias_chain": [], - "resolved_value": "#333333", - "occurrences": 1 - }, - { - "id": "cc99b396695888de", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-800", - "alias_chain": [], - "resolved_value": "#232323", - "occurrences": 1 - }, - { - "id": "a81749b24c76a104", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-gray-900", - "alias_chain": [], - "resolved_value": "#1a1a1a", - "occurrences": 1 - }, - { - "id": "b4ec2e3c309269bc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-red-100", - "alias_chain": [], - "resolved_value": "#ff6b6b", - "occurrences": 1 - }, - { - "id": "fc72b7932957bc7d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-red-200", - "alias_chain": [], - "resolved_value": "#f94b4b", - "occurrences": 1 - }, - { - "id": "69f8b382d32a8a1a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-blue-100", - "alias_chain": [], - "resolved_value": "#7cacff", - "occurrences": 1 - }, - { - "id": "f831f977cfaea1fd", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-blue-200", - "alias_chain": [], - "resolved_value": "#5c98f9", - "occurrences": 1 - }, - { - "id": "e141bbce36f249ff", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-green-100", - "alias_chain": [], - "resolved_value": "#a3d795", - "occurrences": 1 - }, - { - "id": "1255cf8c19286416", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-green-200", - "alias_chain": [], - "resolved_value": "#91cb80", - "occurrences": 1 - }, - { - "id": "2c4f9e980bfc0b65", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-yellow-100", - "alias_chain": [], - "resolved_value": "#ffd966", - "occurrences": 1 - }, - { - "id": "444ea1c8e9a5c8ab", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-yellow-200", - "alias_chain": [], - "resolved_value": "#fbcd44", - "occurrences": 1 - }, - { - "id": "291a77257857e8ad", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-accent", - "alias_chain": ["--color-gray-900 / --color-white"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "1b3152563a7b1147", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-accent", - "alias_chain": ["--color-gray-900 / --color-white"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "e7c5531ba765e5f2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-accent", - "alias_chain": ["--color-gray-900 / --color-white"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "934c32d07615132d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-default", - "alias_chain": ["--color-white / --color-black"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "b693ad9b2277d9a0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-alt", - "alias_chain": ["--color-gray-50 / --color-gray-800"], - "resolved_value": "#f5f5f5", - "occurrences": 1, - "by_theme": { - "light": "#f5f5f5", - "dark": "#232323" - } - }, - { - "id": "34139ab2ec0f72d4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-medium", - "alias_chain": ["--color-gray-400 / --color-gray-700"], - "resolved_value": "#cccccc", - "occurrences": 1, - "by_theme": { - "light": "#cccccc", - "dark": "#333333" - } - }, - { - "id": "ff1b2b05f0c69166", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-muted", - "alias_chain": ["--color-gray-100 / --color-gray-800"], - "resolved_value": "#f0f0f0", - "occurrences": 1, - "by_theme": { - "light": "#f0f0f0", - "dark": "#232323" - } - }, - { - "id": "fcf8367e302c61b3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-inverse", - "alias_chain": ["--color-black / --color-white"], - "resolved_value": "#000000", - "occurrences": 1, - "by_theme": { - "light": "#000000", - "dark": "#ffffff" - } - }, - { - "id": "473c5ff652e7b5c0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-danger", - "alias_chain": ["--color-red-200 / --color-red-100"], - "resolved_value": "#f94b4b", - "occurrences": 1, - "by_theme": { - "light": "#f94b4b", - "dark": "#ff6b6b" - } - }, - { - "id": "7ebba09bb69b0940", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-success", - "alias_chain": ["--color-green-200 / --color-green-100"], - "resolved_value": "#91cb80", - "occurrences": 1, - "by_theme": { - "light": "#91cb80", - "dark": "#a3d795" - } - }, - { - "id": "9894785f3caacab8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-info", - "alias_chain": ["--color-blue-200 / --color-blue-100"], - "resolved_value": "#5c98f9", - "occurrences": 1, - "by_theme": { - "light": "#5c98f9", - "dark": "#7cacff" - } - }, - { - "id": "1f14fe18e860dec2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background-warning", - "alias_chain": ["--color-yellow-200 / --color-yellow-100"], - "resolved_value": "#fbcd44", - "occurrences": 1, - "by_theme": { - "light": "#fbcd44", - "dark": "#ffd966" - } - }, - { - "id": "7365e1375f64ef57", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-default", - "alias_chain": ["--color-gray-200 / --color-gray-700"], - "resolved_value": "#e8e8e8", - "occurrences": 1, - "by_theme": { - "light": "#e8e8e8", - "dark": "#333333" - } - }, - { - "id": "597a84638f4cd32d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-input", - "alias_chain": ["--color-gray-300 / --color-gray-700"], - "resolved_value": "#e5e5e5", - "occurrences": 1, - "by_theme": { - "light": "#e5e5e5", - "dark": "#333333" - } - }, - { - "id": "393c853e77e0dcd5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-input-hover", - "alias_chain": ["--color-gray-400 / --color-gray-600"], - "resolved_value": "#cccccc", - "occurrences": 1, - "by_theme": { - "light": "#cccccc", - "dark": "#666666" - } - }, - { - "id": "da99b85f42e8fce8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-strong", - "alias_chain": ["--color-gray-900 / --color-white"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "3ec2a34be027d3c2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-card", - "alias_chain": ["--color-gray-200 / --color-gray-700"], - "resolved_value": "#e8e8e8", - "occurrences": 1, - "by_theme": { - "light": "#e8e8e8", - "dark": "#333333" - } - }, - { - "id": "03efc0dd68a9c864", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-inverse", - "alias_chain": ["--color-black / --color-white"], - "resolved_value": "#000000", - "occurrences": 1, - "by_theme": { - "light": "#000000", - "dark": "#ffffff" - } - }, - { - "id": "65b6f9d0dcda9178", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-danger", - "alias_chain": ["--color-red-200"], - "resolved_value": "#f94b4b", - "occurrences": 1, - "by_theme": { - "light": "#f94b4b", - "dark": "#f94b4b" - } - }, - { - "id": "b61e38195993cd88", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-success", - "alias_chain": ["--color-green-200"], - "resolved_value": "#91cb80", - "occurrences": 1, - "by_theme": { - "light": "#91cb80", - "dark": "#91cb80" - } - }, - { - "id": "f811d3c5dc3a6e84", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-warning", - "alias_chain": ["--color-yellow-200"], - "resolved_value": "#fbcd44", - "occurrences": 1, - "by_theme": { - "light": "#fbcd44", - "dark": "#fbcd44" - } - }, - { - "id": "c132a09c244ef7be", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border-info", - "alias_chain": ["--color-blue-200"], - "resolved_value": "#5c98f9", - "occurrences": 1, - "by_theme": { - "light": "#5c98f9", - "dark": "#5c98f9" - } - }, - { - "id": "a75e4ad678edacb9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-default", - "alias_chain": ["--color-gray-900 / --color-white"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "dee63e3ec1bc7cff", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-muted", - "alias_chain": ["--color-gray-500"], - "resolved_value": "#999999", - "occurrences": 1, - "by_theme": { - "light": "#999999", - "dark": "#999999" - } - }, - { - "id": "a5d84cdfd41dec1c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-alt", - "alias_chain": ["--color-gray-600 / --color-gray-500"], - "resolved_value": "#666666", - "occurrences": 1, - "by_theme": { - "light": "#666666", - "dark": "#999999" - } - }, - { - "id": "e53cb3a48c2d9256", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-inverse", - "alias_chain": ["--color-white / --color-black"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "b3f7fb8c0e4f3be6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-danger", - "alias_chain": ["--color-red-200 / --color-red-100"], - "resolved_value": "#f94b4b", - "occurrences": 1, - "by_theme": { - "light": "#f94b4b", - "dark": "#ff6b6b" - } - }, - { - "id": "d7774891b741f0ef", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-success", - "alias_chain": ["--color-green-200 / --color-green-100"], - "resolved_value": "#91cb80", - "occurrences": 1, - "by_theme": { - "light": "#91cb80", - "dark": "#a3d795" - } - }, - { - "id": "f812d94d9c3b802c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-warning", - "alias_chain": ["--color-yellow-200 / --color-yellow-100"], - "resolved_value": "#fbcd44", - "occurrences": 1, - "by_theme": { - "light": "#fbcd44", - "dark": "#ffd966" - } - }, - { - "id": "d7aadea557e5693b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-info", - "alias_chain": ["--color-blue-200 / --color-blue-100"], - "resolved_value": "#5c98f9", - "occurrences": 1, - "by_theme": { - "light": "#5c98f9", - "dark": "#7cacff" - } - }, - { - "id": "303c69b6cfa35d9e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--ring", - "alias_chain": ["--border-strong"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "bd5e159576c7fb2c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--dark-10", - "alias_chain": [], - "resolved_value": "rgba(26, 26, 26, 0.1)", - "occurrences": 1, - "by_theme": { - "light": "rgba(26, 26, 26, 0.1)", - "dark": "rgba(242, 242, 242, 0.1)" - } - }, - { - "id": "d1cfb296dbf4b1d2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--dark-40", - "alias_chain": [], - "resolved_value": "rgba(26, 26, 26, 0.4)", - "occurrences": 1, - "by_theme": { - "light": "rgba(26, 26, 26, 0.4)", - "dark": "rgba(242, 242, 242, 0.4)" - } - }, - { - "id": "a97fcfec1758dc06", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--dark-04", - "alias_chain": [], - "resolved_value": "rgba(26, 26, 26, 0.04)", - "occurrences": 1, - "by_theme": { - "light": "rgba(26, 26, 26, 0.04)", - "dark": "rgba(242, 242, 242, 0.04)" - } - }, - { - "id": "233d449d3d3cc60e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-mini", - "alias_chain": [], - "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", - "occurrences": 1, - "by_theme": { - "light": "0 2px 8px rgba(76, 76, 76, 0.15)", - "dark": "0 2px 8px rgba(0, 0, 0, 0.4)" - } - }, - { - "id": "5d696aaf8c3df3b4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-mini-inset", - "alias_chain": [], - "resolved_value": "0 1px 4px rgba(76, 76, 76, 0.1) inset", - "occurrences": 1, - "by_theme": { - "light": "0 1px 4px rgba(76, 76, 76, 0.1) inset", - "dark": "0 1px 4px rgba(0, 0, 0, 0.5) inset" - } - }, - { - "id": "c3f67cd72d4329b7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-btn", - "alias_chain": [], - "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", - "occurrences": 1, - "by_theme": { - "light": "0 2px 8px rgba(76, 76, 76, 0.15)", - "dark": "0 2px 8px rgba(0, 0, 0, 0.3)" - } - }, - { - "id": "75d95ead01c2acc2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-card", - "alias_chain": [], - "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", - "occurrences": 1, - "by_theme": { - "light": "0 2px 8px rgba(76, 76, 76, 0.15)", - "dark": "0 2px 8px rgba(0, 0, 0, 0.4)" - } - }, - { - "id": "87aafefae1878398", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-elevated", - "alias_chain": [], - "resolved_value": "0 3px 12px rgba(76, 76, 76, 0.22)", - "occurrences": 1, - "by_theme": { - "light": "0 3px 12px rgba(76, 76, 76, 0.22)", - "dark": "0 3px 12px rgba(0, 0, 0, 0.5)" - } - }, - { - "id": "8ccbd4dd77127474", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-popover", - "alias_chain": [], - "resolved_value": "0 8px 30px rgba(0, 0, 0, 0.12)", - "occurrences": 1, - "by_theme": { - "light": "0 8px 30px rgba(0, 0, 0, 0.12)", - "dark": "0 8px 30px rgba(0, 0, 0, 0.4)" - } - }, - { - "id": "195642d6fbb9efb4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-modal", - "alias_chain": [], - "resolved_value": "0 20px 60px rgba(0, 0, 0, 0.2)", - "occurrences": 1, - "by_theme": { - "light": "0 20px 60px rgba(0, 0, 0, 0.2)", - "dark": "0 20px 60px rgba(0, 0, 0, 0.6)" - } - }, - { - "id": "315f81ba18c7be3a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-kbd", - "alias_chain": [], - "resolved_value": "0 2px 8px rgba(76, 76, 76, 0.15)", - "occurrences": 1, - "by_theme": { - "light": "0 2px 8px rgba(76, 76, 76, 0.15)", - "dark": "0 2px 8px rgba(0, 0, 0, 0.4)" - } - }, - { - "id": "56a711f52f8591da", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--shadow-date-field-focus", - "alias_chain": [], - "resolved_value": "0 0 0 3px rgba(26, 26, 26, 0.15)", - "occurrences": 1, - "by_theme": { - "light": "0 0 0 3px rgba(26, 26, 26, 0.15)", - "dark": "0 0 0 3px rgba(244, 244, 245, 0.1)" - } - }, - { - "id": "c7e300d68bdaacfd", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--surface-dark", - "alias_chain": [], - "resolved_value": "#0a0a0a", - "occurrences": 1 - }, - { - "id": "ca975ff64e882a94", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--surface-dark-text", - "alias_chain": [], - "resolved_value": "#f5f5f5", - "occurrences": 1 - }, - { - "id": "bd8eb290d3d9ac61", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--surface-dark-muted", - "alias_chain": [], - "resolved_value": "rgba(255, 255, 255, 0.5)", - "occurrences": 1 - }, - { - "id": "f3f92671144f3aaf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--surface-dark-border", - "alias_chain": [], - "resolved_value": "rgba(255, 255, 255, 0.08)", - "occurrences": 1 - }, - { - "id": "0d9871656df09d48", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--chart-1", - "alias_chain": [], - "resolved_value": "#f6b44a", - "occurrences": 1 - }, - { - "id": "ed72059040617b99", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--chart-2", - "alias_chain": [], - "resolved_value": "#7585ff", - "occurrences": 1 - }, - { - "id": "fbdc26dfc312d9bd", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--chart-3", - "alias_chain": [], - "resolved_value": "#d76a6a", - "occurrences": 1 - }, - { - "id": "d1092d429926c42e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--chart-4", - "alias_chain": [], - "resolved_value": "#d185e0", - "occurrences": 1 - }, - { - "id": "33d2ab78b0fd5a67", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--chart-5", - "alias_chain": [], - "resolved_value": "#91cb80", - "occurrences": 1 - }, - { - "id": "1a657d821db05b18", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--background", - "alias_chain": ["--background-default"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "73658c69a6e8eee9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--foreground", - "alias_chain": ["--text-default"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "808765357c11757b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--card", - "alias_chain": ["--background-default"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "8e17fb7c3d634b52", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--card-foreground", - "alias_chain": ["--text-default"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "34d0849d48012424", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--popover", - "alias_chain": ["--background-default"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "d30e7aa452f2c9aa", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--popover-foreground", - "alias_chain": ["--text-default"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "500066d5f74a8e84", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--primary", - "alias_chain": [ - "--background-accent", - "--color-gray-900 / --color-white" - ], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "3f897e3533c5af0e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--primary-foreground", - "alias_chain": ["--text-inverse"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "5418fc11cc0bce11", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--secondary", - "alias_chain": ["--background-muted"], - "resolved_value": "#f0f0f0", - "occurrences": 1, - "by_theme": { - "light": "#f0f0f0", - "dark": "#232323" - } - }, - { - "id": "e8ff62d0ef7ad218", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--secondary-foreground", - "alias_chain": ["--text-default"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "c0570a61373a0bff", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--muted", - "alias_chain": ["--background-muted"], - "resolved_value": "#f0f0f0", - "occurrences": 1, - "by_theme": { - "light": "#f0f0f0", - "dark": "#232323" - } - }, - { - "id": "d6dd3bb494fad90b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--muted-foreground", - "alias_chain": ["--text-muted"], - "resolved_value": "#999999", - "occurrences": 1, - "by_theme": { - "light": "#999999", - "dark": "#999999" - } - }, - { - "id": "2f5af28bc37168d8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--accent", - "alias_chain": ["--background-muted"], - "resolved_value": "#f0f0f0", - "occurrences": 1, - "by_theme": { - "light": "#f0f0f0", - "dark": "#232323" - } - }, - { - "id": "611e42252b73efc1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--accent-foreground", - "alias_chain": ["--text-default"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "ae2830a2646b8f8f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--destructive", - "alias_chain": ["--background-danger"], - "resolved_value": "#f94b4b", - "occurrences": 1, - "by_theme": { - "light": "#f94b4b", - "dark": "#ff6b6b" - } - }, - { - "id": "c41a2c4ea83c794e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--destructive-foreground", - "alias_chain": [], - "resolved_value": "#ffffff", - "occurrences": 1 - }, - { - "id": "dd9400a20e5a499c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--border", - "alias_chain": ["--border-default"], - "resolved_value": "#e8e8e8", - "occurrences": 1, - "by_theme": { - "light": "#e8e8e8", - "dark": "#333333" - } - }, - { - "id": "89a847a237b42db5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--input", - "alias_chain": ["--border-input"], - "resolved_value": "#e5e5e5", - "occurrences": 1, - "by_theme": { - "light": "#e5e5e5", - "dark": "#333333" - } - }, - { - "id": "7424706a0db1c7de", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar", - "alias_chain": ["--background-default"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "df9686b6be2dc08e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar-foreground", - "alias_chain": ["--text-default"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "7aed6df9bb686b70", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar-primary", - "alias_chain": ["--background-accent"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "bbeaf4ac78ef5a1d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar-primary-foreground", - "alias_chain": ["--text-inverse"], - "resolved_value": "#ffffff", - "occurrences": 1, - "by_theme": { - "light": "#ffffff", - "dark": "#000000" - } - }, - { - "id": "56aa3e404f934e60", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar-accent", - "alias_chain": ["--background-muted"], - "resolved_value": "#f0f0f0", - "occurrences": 1, - "by_theme": { - "light": "#f0f0f0", - "dark": "#232323" - } - }, - { - "id": "d6978087b674442c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar-accent-foreground", - "alias_chain": ["--text-default"], - "resolved_value": "#1a1a1a", - "occurrences": 1, - "by_theme": { - "light": "#1a1a1a", - "dark": "#ffffff" - } - }, - { - "id": "596b04983aeb3b98", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar-border", - "alias_chain": ["--border-default"], - "resolved_value": "#e8e8e8", - "occurrences": 1, - "by_theme": { - "light": "#e8e8e8", - "dark": "#333333" - } - }, - { - "id": "834707bedd7948e1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--sidebar-ring", - "alias_chain": ["--border-default"], - "resolved_value": "#e8e8e8", - "occurrences": 1, - "by_theme": { - "light": "#e8e8e8", - "dark": "#333333" - } - }, - { - "id": "d6b2ba8870988f84", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-display-font-size", - "alias_chain": [], - "resolved_value": "clamp(64px, 8vw, 96px)", - "occurrences": 1 - }, - { - "id": "2727d13f1cfe39d6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-display-line-height", - "alias_chain": [], - "resolved_value": "0.88", - "occurrences": 1 - }, - { - "id": "8c5f7b3ba48f05b5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-display-letter-spacing", - "alias_chain": [], - "resolved_value": "-0.05em", - "occurrences": 1 - }, - { - "id": "3b3300ccdd19ec7d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-display-font-weight", - "alias_chain": [], - "resolved_value": "900", - "occurrences": 1 - }, - { - "id": "9597aa218b3118f1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-section-font-size", - "alias_chain": [], - "resolved_value": "clamp(44px, 5vw, 64px)", - "occurrences": 1 - }, - { - "id": "cb36a036101a7d6c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-section-line-height", - "alias_chain": [], - "resolved_value": "0.95", - "occurrences": 1 - }, - { - "id": "aeb269ff49d4c792", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-section-letter-spacing", - "alias_chain": [], - "resolved_value": "-0.035em", - "occurrences": 1 - }, - { - "id": "4c3dcba84ada6b40", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-section-font-weight", - "alias_chain": [], - "resolved_value": "700", - "occurrences": 1 - }, - { - "id": "90e19323037a9e4b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-sub-font-size", - "alias_chain": [], - "resolved_value": "clamp(28px, 3vw, 40px)", - "occurrences": 1 - }, - { - "id": "4dcf68120d56349f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-sub-line-height", - "alias_chain": [], - "resolved_value": "1", - "occurrences": 1 - }, - { - "id": "282451bf14000aca", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-sub-letter-spacing", - "alias_chain": [], - "resolved_value": "-0.02em", - "occurrences": 1 - }, - { - "id": "44481b55a6c428c0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-sub-font-weight", - "alias_chain": [], - "resolved_value": "700", - "occurrences": 1 - }, - { - "id": "fd79e33b4745daa2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-card-font-size", - "alias_chain": [], - "resolved_value": "clamp(20px, 2vw, 28px)", - "occurrences": 1 - }, - { - "id": "e7995cdde7903eb7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-card-line-height", - "alias_chain": [], - "resolved_value": "1.1", - "occurrences": 1 - }, - { - "id": "7805a861b697a244", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-card-letter-spacing", - "alias_chain": [], - "resolved_value": "-0.01em", - "occurrences": 1 - }, - { - "id": "c937175a2c87183b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--heading-card-font-weight", - "alias_chain": [], - "resolved_value": "600", - "occurrences": 1 - }, - { - "id": "d56b5fd640d91d2e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--display-size", - "alias_chain": [], - "resolved_value": "clamp(3rem, 12vw, 12rem)", - "occurrences": 1 - }, - { - "id": "0830bc59a070152e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--display-line-height", - "alias_chain": [], - "resolved_value": "0.85", - "occurrences": 1 - }, - { - "id": "80c7e6c5f2f69c3a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--display-letter-spacing", - "alias_chain": [], - "resolved_value": "-0.05em", - "occurrences": 1 - }, - { - "id": "853e708cd3ec8a13", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--body-reading-size", - "alias_chain": [], - "resolved_value": "clamp(1rem, 1.3vw, 1.25rem)", - "occurrences": 1 - }, - { - "id": "68527d95d220d0b8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--body-reading-line-height", - "alias_chain": [], - "resolved_value": "1.65", - "occurrences": 1 - }, - { - "id": "fba00f57281b37b6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--body-reading-letter-spacing", - "alias_chain": [], - "resolved_value": "-0.01em", - "occurrences": 1 - }, - { - "id": "34ebf898845dff00", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--label-font-size", - "alias_chain": [], - "resolved_value": "11px", - "occurrences": 1 - }, - { - "id": "bd3ab2b8dc7b4a41", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--label-letter-spacing", - "alias_chain": [], - "resolved_value": "0.12em", - "occurrences": 1 - }, - { - "id": "1ac3d9b03387e64a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--label-font-weight", - "alias_chain": [], - "resolved_value": "600", - "occurrences": 1 - }, - { - "id": "82bca8aecab68f76", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--label-line-height", - "alias_chain": [], - "resolved_value": "1.2", - "occurrences": 1 - }, - { - "id": "966b15a2c71e21e5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--pullquote-size", - "alias_chain": [], - "resolved_value": "clamp(1.5rem, 3vw, 2.5rem)", - "occurrences": 1 - }, - { - "id": "70ab43d940d8e1d8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--pullquote-line-height", - "alias_chain": [], - "resolved_value": "1.3", - "occurrences": 1 - }, - { - "id": "a3c707a33d947c62", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--pullquote-weight", - "alias_chain": [], - "resolved_value": "300", - "occurrences": 1 - }, - { - "id": "01e3c9532606c7db", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--pullquote-letter-spacing", - "alias_chain": [], - "resolved_value": "-0.02em", - "occurrences": 1 - }, - { - "id": "f6a70c8d2860701f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--page-container-max-width", - "alias_chain": [], - "resolved_value": "1440px", - "occurrences": 1 - }, - { - "id": "bcd63f02e70cf4c8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--page-container-side-gutter", - "alias_chain": [], - "resolved_value": "20px", - "occurrences": 1 - }, - { - "id": "854301d64eff7607", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--section-padding-vertical", - "alias_chain": [], - "resolved_value": "100px", - "occurrences": 1 - }, - { - "id": "05193897ce5f6251", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--section-heading-margin-bottom", - "alias_chain": [], - "resolved_value": "75px", - "occurrences": 1 - }, - { - "id": "c3c04dd0845ab2db", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--ease-spring", - "alias_chain": [], - "resolved_value": "cubic-bezier(0.33, 1, 0.68, 1)", - "occurrences": 1 - }, - { - "id": "153b5f16aa162a51", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--duration-fast", - "alias_chain": [], - "resolved_value": "0.15s", - "occurrences": 1 - }, - { - "id": "8ec16c70d9ef3bca", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--duration-normal", - "alias_chain": [], - "resolved_value": "0.2s", - "occurrences": 1 - }, - { - "id": "6ce12051c43416c7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--duration-slow", - "alias_chain": [], - "resolved_value": "0.4s", - "occurrences": 1 - }, - { - "id": "ba90a9ff4efe7583", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background", - "alias_chain": ["--background"], - "resolved_value": "var(--background)", - "occurrences": 1 - }, - { - "id": "b85438d94a31c876", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-foreground", - "alias_chain": ["--foreground"], - "resolved_value": "var(--foreground)", - "occurrences": 1 - }, - { - "id": "2af1401d0420f747", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-card", - "alias_chain": ["--card"], - "resolved_value": "var(--card)", - "occurrences": 1 - }, - { - "id": "102cac697c9c7f63", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-card-foreground", - "alias_chain": ["--card-foreground"], - "resolved_value": "var(--card-foreground)", - "occurrences": 1 - }, - { - "id": "5b87dd9a9341cfa3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-popover", - "alias_chain": ["--popover"], - "resolved_value": "var(--popover)", - "occurrences": 1 - }, - { - "id": "d78974392e2775bf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-popover-foreground", - "alias_chain": ["--popover-foreground"], - "resolved_value": "var(--popover-foreground)", - "occurrences": 1 - }, - { - "id": "c05f932a45e9e249", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-primary", - "alias_chain": ["--primary"], - "resolved_value": "var(--primary)", - "occurrences": 1 - }, - { - "id": "c5e8f8330a9a68bb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-primary-foreground", - "alias_chain": ["--primary-foreground"], - "resolved_value": "var(--primary-foreground)", - "occurrences": 1 - }, - { - "id": "4bd74242e4fa9f9d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-secondary", - "alias_chain": ["--secondary"], - "resolved_value": "var(--secondary)", - "occurrences": 1 - }, - { - "id": "da63d7508b5c05f2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-secondary-foreground", - "alias_chain": ["--secondary-foreground"], - "resolved_value": "var(--secondary-foreground)", - "occurrences": 1 - }, - { - "id": "de4a144ab9575566", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-muted", - "alias_chain": ["--muted"], - "resolved_value": "var(--muted)", - "occurrences": 1 - }, - { - "id": "71a45bf6c6c9dd9f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-muted-foreground", - "alias_chain": ["--muted-foreground"], - "resolved_value": "var(--muted-foreground)", - "occurrences": 1 - }, - { - "id": "85ecb83cbec28f93", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-accent", - "alias_chain": ["--accent"], - "resolved_value": "var(--accent)", - "occurrences": 1 - }, - { - "id": "d2bfe49e403e78a7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-accent-foreground", - "alias_chain": ["--accent-foreground"], - "resolved_value": "var(--accent-foreground)", - "occurrences": 1 - }, - { - "id": "09a4ebfe8ca10a58", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-destructive", - "alias_chain": ["--destructive"], - "resolved_value": "var(--destructive)", - "occurrences": 1 - }, - { - "id": "2d5aa7677adfa312", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-destructive-foreground", - "alias_chain": ["--destructive-foreground"], - "resolved_value": "var(--destructive-foreground)", - "occurrences": 1 - }, - { - "id": "b122514be83994cb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border", - "alias_chain": ["--border"], - "resolved_value": "var(--border)", - "occurrences": 1 - }, - { - "id": "c9daaf8f106c08b6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-input", - "alias_chain": ["--input"], - "resolved_value": "var(--input)", - "occurrences": 1 - }, - { - "id": "8de30c336c23097b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-default", - "alias_chain": ["--background-default"], - "resolved_value": "var(--background-default)", - "occurrences": 1 - }, - { - "id": "cac50d9d5e7060b1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-alt", - "alias_chain": ["--background-alt"], - "resolved_value": "var(--background-alt)", - "occurrences": 1 - }, - { - "id": "e1dfc17379ecca5e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-medium", - "alias_chain": ["--background-medium"], - "resolved_value": "var(--background-medium)", - "occurrences": 1 - }, - { - "id": "b6bef8606d9c854c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-inverse", - "alias_chain": ["--background-inverse"], - "resolved_value": "var(--background-inverse)", - "occurrences": 1 - }, - { - "id": "cf144314e1af1329", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-muted", - "alias_chain": ["--background-muted"], - "resolved_value": "var(--background-muted)", - "occurrences": 1 - }, - { - "id": "c27cdfc6a33f1a59", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-danger", - "alias_chain": ["--background-danger"], - "resolved_value": "var(--background-danger)", - "occurrences": 1 - }, - { - "id": "f9a63a1b88d0d820", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-success", - "alias_chain": ["--background-success"], - "resolved_value": "var(--background-success)", - "occurrences": 1 - }, - { - "id": "bdbf5d1308013dcf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-info", - "alias_chain": ["--background-info"], - "resolved_value": "var(--background-info)", - "occurrences": 1 - }, - { - "id": "c8a78e6b4bb4fab5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-warning", - "alias_chain": ["--background-warning"], - "resolved_value": "var(--background-warning)", - "occurrences": 1 - }, - { - "id": "e341770f823c8162", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-background-accent", - "alias_chain": ["--background-accent"], - "resolved_value": "var(--background-accent)", - "occurrences": 1 - }, - { - "id": "268d438bbe3fbcf2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-accent", - "alias_chain": ["--border-accent"], - "resolved_value": "var(--border-accent)", - "occurrences": 1 - }, - { - "id": "597c0c88282e7d8b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-accent", - "alias_chain": ["--text-accent"], - "resolved_value": "var(--text-accent)", - "occurrences": 1 - }, - { - "id": "da08f02f3d7b08f3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-default", - "alias_chain": ["--border-default"], - "resolved_value": "var(--border-default)", - "occurrences": 1 - }, - { - "id": "18c2c370231cf066", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-input", - "alias_chain": ["--border-input"], - "resolved_value": "var(--border-input)", - "occurrences": 1 - }, - { - "id": "179f16855617f273", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-input-hover", - "alias_chain": ["--border-input-hover"], - "resolved_value": "var(--border-input-hover)", - "occurrences": 1 - }, - { - "id": "0b4af0e11b07e5f3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-strong", - "alias_chain": ["--border-strong"], - "resolved_value": "var(--border-strong)", - "occurrences": 1 - }, - { - "id": "068fdd966f2422d1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-card", - "alias_chain": ["--border-card"], - "resolved_value": "var(--border-card)", - "occurrences": 1 - }, - { - "id": "8982b837528ff079", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-inverse", - "alias_chain": ["--border-inverse"], - "resolved_value": "var(--border-inverse)", - "occurrences": 1 - }, - { - "id": "217576c0840e42d2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-danger", - "alias_chain": ["--border-danger"], - "resolved_value": "var(--border-danger)", - "occurrences": 1 - }, - { - "id": "7618470d59758faf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-success", - "alias_chain": ["--border-success"], - "resolved_value": "var(--border-success)", - "occurrences": 1 - }, - { - "id": "eaecad0b7dff6f2c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-warning", - "alias_chain": ["--border-warning"], - "resolved_value": "var(--border-warning)", - "occurrences": 1 - }, - { - "id": "de06e446c926aeb0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-border-info", - "alias_chain": ["--border-info"], - "resolved_value": "var(--border-info)", - "occurrences": 1 - }, - { - "id": "e873b538ed81e5b3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-default", - "alias_chain": ["--text-default"], - "resolved_value": "var(--text-default)", - "occurrences": 1 - }, - { - "id": "7943607583511f4e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-muted", - "alias_chain": ["--text-muted"], - "resolved_value": "var(--text-muted)", - "occurrences": 1 - }, - { - "id": "64255472c6e16d79", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-alt", - "alias_chain": ["--text-alt"], - "resolved_value": "var(--text-alt)", - "occurrences": 1 - }, - { - "id": "8f57d85c05f80ffe", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-inverse", - "alias_chain": ["--text-inverse"], - "resolved_value": "var(--text-inverse)", - "occurrences": 1 - }, - { - "id": "a946f6573b264cdc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-danger", - "alias_chain": ["--text-danger"], - "resolved_value": "var(--text-danger)", - "occurrences": 1 - }, - { - "id": "2a992f9761701071", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-success", - "alias_chain": ["--text-success"], - "resolved_value": "var(--text-success)", - "occurrences": 1 - }, - { - "id": "89b4e107d81fa054", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-warning", - "alias_chain": ["--text-warning"], - "resolved_value": "var(--text-warning)", - "occurrences": 1 - }, - { - "id": "d2e999b06fcc54bf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-text-info", - "alias_chain": ["--text-info"], - "resolved_value": "var(--text-info)", - "occurrences": 1 - }, - { - "id": "d192ce28c423cb6d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-dark-10", - "alias_chain": ["--dark-10"], - "resolved_value": "var(--dark-10)", - "occurrences": 1 - }, - { - "id": "e9c4d9b318f6e1be", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-dark-40", - "alias_chain": ["--dark-40"], - "resolved_value": "var(--dark-40)", - "occurrences": 1 - }, - { - "id": "2e27f6135dcee415", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-dark-04", - "alias_chain": ["--dark-04"], - "resolved_value": "var(--dark-04)", - "occurrences": 1 - }, - { - "id": "d7cbcdae48159a70", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-surface-dark", - "alias_chain": ["--surface-dark"], - "resolved_value": "var(--surface-dark)", - "occurrences": 1 - }, - { - "id": "19c8a500fbf1b6ef", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-surface-dark-text", - "alias_chain": ["--surface-dark-text"], - "resolved_value": "var(--surface-dark-text)", - "occurrences": 1 - }, - { - "id": "309e50bf3390fe9c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-surface-dark-muted", - "alias_chain": ["--surface-dark-muted"], - "resolved_value": "var(--surface-dark-muted)", - "occurrences": 1 - }, - { - "id": "83db72b2fe4bf407", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-surface-dark-border", - "alias_chain": ["--surface-dark-border"], - "resolved_value": "var(--surface-dark-border)", - "occurrences": 1 - }, - { - "id": "19a1f07ded06a4ff", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-ring", - "alias_chain": ["--ring"], - "resolved_value": "var(--ring)", - "occurrences": 1 - }, - { - "id": "756bfcab5a8992fa", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-chart-1", - "alias_chain": ["--chart-1"], - "resolved_value": "var(--chart-1)", - "occurrences": 1 - }, - { - "id": "d1670075fe3e0dc9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-chart-2", - "alias_chain": ["--chart-2"], - "resolved_value": "var(--chart-2)", - "occurrences": 1 - }, - { - "id": "8a84b2545a76b5ad", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-chart-3", - "alias_chain": ["--chart-3"], - "resolved_value": "var(--chart-3)", - "occurrences": 1 - }, - { - "id": "c9ceb1f1fe1512d8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-chart-4", - "alias_chain": ["--chart-4"], - "resolved_value": "var(--chart-4)", - "occurrences": 1 - }, - { - "id": "95b74ea12502c33a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-chart-5", - "alias_chain": ["--chart-5"], - "resolved_value": "var(--chart-5)", - "occurrences": 1 - }, - { - "id": "755621460c18185a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar", - "alias_chain": ["--sidebar"], - "resolved_value": "var(--sidebar)", - "occurrences": 1 - }, - { - "id": "afb7072c4022b739", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar-foreground", - "alias_chain": ["--sidebar-foreground"], - "resolved_value": "var(--sidebar-foreground)", - "occurrences": 1 - }, - { - "id": "0c5472432a417c50", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar-primary", - "alias_chain": ["--sidebar-primary"], - "resolved_value": "var(--sidebar-primary)", - "occurrences": 1 - }, - { - "id": "dec96334a5a9baab", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar-primary-foreground", - "alias_chain": ["--sidebar-primary-foreground"], - "resolved_value": "var(--sidebar-primary-foreground)", - "occurrences": 1 - }, - { - "id": "7380feaccefcb3e5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar-accent", - "alias_chain": ["--sidebar-accent"], - "resolved_value": "var(--sidebar-accent)", - "occurrences": 1 - }, - { - "id": "75fe6fec18863906", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar-accent-foreground", - "alias_chain": ["--sidebar-accent-foreground"], - "resolved_value": "var(--sidebar-accent-foreground)", - "occurrences": 1 - }, - { - "id": "28547c482af9c490", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar-border", - "alias_chain": ["--sidebar-border"], - "resolved_value": "var(--sidebar-border)", - "occurrences": 1 - }, - { - "id": "c45db65b24a8ada8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--color-sidebar-ring", - "alias_chain": ["--sidebar-ring"], - "resolved_value": "var(--sidebar-ring)", - "occurrences": 1 - }, - { - "id": "2c4c4e3b65e4b9cc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--font-sans", - "alias_chain": [], - "resolved_value": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", - "occurrences": 1 - }, - { - "id": "994655d63e522cc4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--font-mono", - "alias_chain": [], - "resolved_value": "'Geist Mono', monospace", - "occurrences": 1 - }, - { - "id": "797f64068decb091", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--font-serif", - "alias_chain": [], - "resolved_value": "serif", - "occurrences": 1 - }, - { - "id": "cdd3868eaea7c3cf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--font-display", - "alias_chain": [], - "resolved_value": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", - "occurrences": 1 - }, - { - "id": "411e8d14ce898e46", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius", - "alias_chain": [], - "resolved_value": "20px", - "occurrences": 1 - }, - { - "id": "ae926ffd763b060e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-pill", - "alias_chain": [], - "resolved_value": "999px", - "occurrences": 1 - }, - { - "id": "6227932d8b117253", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-button", - "alias_chain": [], - "resolved_value": "999px", - "occurrences": 1 - }, - { - "id": "930f5fe8abe418b7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-input", - "alias_chain": [], - "resolved_value": "999px", - "occurrences": 1 - }, - { - "id": "1939cf325e6fb62c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-card", - "alias_chain": [], - "resolved_value": "20px", - "occurrences": 1 - }, - { - "id": "6fe9a8e2c233e5e0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-card-lg", - "alias_chain": [], - "resolved_value": "24px", - "occurrences": 1 - }, - { - "id": "f195b5f308ecfc8b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-card-sm", - "alias_chain": [], - "resolved_value": "14px", - "occurrences": 1 - }, - { - "id": "b22d2e872418b9d8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-dropdown", - "alias_chain": [], - "resolved_value": "10px", - "occurrences": 1 - }, - { - "id": "bf1ad74ed6936272", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-modal", - "alias_chain": [], - "resolved_value": "16px", - "occurrences": 1 - }, - { - "id": "569e0342e47d4374", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-sm", - "alias_chain": ["--radius"], - "resolved_value": "16px", - "occurrences": 1 - }, - { - "id": "4eaab123ab61d0e3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-md", - "alias_chain": ["--radius"], - "resolved_value": "18px", - "occurrences": 1 - }, - { - "id": "05c13b89554f36d7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-lg", - "alias_chain": ["--radius"], - "resolved_value": "20px", - "occurrences": 1 - }, - { - "id": "f012f054d405e98c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--radius-xl", - "alias_chain": ["--radius"], - "resolved_value": "24px", - "occurrences": 1 - }, - { - "id": "ac19388f4f5c9e9a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--spacing-input", - "alias_chain": [], - "resolved_value": "3.25rem", - "occurrences": 1 - }, - { - "id": "5dfbdbeda6b40580", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--spacing-input-sm", - "alias_chain": [], - "resolved_value": "2.75rem", - "occurrences": 1 - }, - { - "id": "c6d683f67359c12c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--spacing-button", - "alias_chain": [], - "resolved_value": "2.75rem", - "occurrences": 1 - }, - { - "id": "c68e39f240797dab", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--spacing-button-sm", - "alias_chain": [], - "resolved_value": "2rem", - "occurrences": 1 - }, - { - "id": "495518f7a17ef7c5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--text-xxs", - "alias_chain": [], - "resolved_value": "10px", - "occurrences": 1 - }, - { - "id": "88bb5cb22565b321", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--breakpoint-desktop", - "alias_chain": [], - "resolved_value": "1440px", - "occurrences": 1 - }, - { - "id": "b7b73a94200d4804", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-accordion-down", - "alias_chain": [], - "resolved_value": "accordion-down 0.2s ease-out", - "occurrences": 1 - }, - { - "id": "57034a94bc88e317", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-accordion-up", - "alias_chain": [], - "resolved_value": "accordion-up 0.2s ease-out", - "occurrences": 1 - }, - { - "id": "7c2176f7d1142760", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-caret-blink", - "alias_chain": [], - "resolved_value": "caret-blink 1s ease-out infinite", - "occurrences": 1 - }, - { - "id": "c0d6746194554f8b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-scale-in", - "alias_chain": [], - "resolved_value": "scale-in 0.2s ease", - "occurrences": 1 - }, - { - "id": "177656823c1abb88", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-scale-out", - "alias_chain": [], - "resolved_value": "scale-out 0.15s ease", - "occurrences": 1 - }, - { - "id": "608f095758e4ad69", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-fade-in", - "alias_chain": [], - "resolved_value": "fade-in 0.2s ease", - "occurrences": 1 - }, - { - "id": "1635e03f0cd75ee9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-fade-out", - "alias_chain": [], - "resolved_value": "fade-out 0.15s ease", - "occurrences": 1 - }, - { - "id": "6047efe8fe99c4f1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-enter-from-left", - "alias_chain": [], - "resolved_value": "enter-from-left 0.2s ease", - "occurrences": 1 - }, - { - "id": "d80ddbeef4af3076", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-enter-from-right", - "alias_chain": [], - "resolved_value": "enter-from-right 0.2s ease", - "occurrences": 1 - }, - { - "id": "f291d97007a89ac9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-exit-to-left", - "alias_chain": [], - "resolved_value": "exit-to-left 0.2s ease", - "occurrences": 1 - }, - { - "id": "3becf0bbd8bf2257", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-exit-to-right", - "alias_chain": [], - "resolved_value": "exit-to-right 0.2s ease", - "occurrences": 1 - }, - { - "id": "bf297ba3f1bce3a9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "--animate-word-reveal", - "alias_chain": [], - "resolved_value": "word-reveal 0.4s ease-out", - "occurrences": 1 - } - ], - "components": [ - { - "id": "29b3ff98f131fdb2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "ghost-ui-base", - "discovered_via": "registry.json" - }, - { - "id": "e5d77dfea43a03a0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "styles-main", - "discovered_via": "registry.json" - }, - { - "id": "169b869aa9181351", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "font-faces", - "discovered_via": "registry.json" - }, - { - "id": "ddc1aa0abe4996d4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "utils", - "discovered_via": "registry.json" - }, - { - "id": "97316b0a2365edbe", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "theme-warm-sand", - "discovered_via": "registry.json" - }, - { - "id": "47f13968c74e8a1e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "theme-ocean", - "discovered_via": "registry.json" - }, - { - "id": "97bba1fad769c640", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "theme-midnight-luxe", - "discovered_via": "registry.json" - }, - { - "id": "35218ab39aab0000", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "theme-neon-brutalist", - "discovered_via": "registry.json" - }, - { - "id": "103cac0ebc97a2cf", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "theme-soft-pastel", - "discovered_via": "registry.json" - }, - { - "id": "df3f7d47030f9067", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "accordion", - "discovered_via": "registry.json" - }, - { - "id": "69a50ee808363ef3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "alert-dialog", - "discovered_via": "registry.json" - }, - { - "id": "cea13e15d155c552", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "alert", - "discovered_via": "registry.json", - "variants": ["default", "destructive"] - }, - { - "id": "7cb029fbb92f75eb", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "aspect-ratio", - "discovered_via": "registry.json" - }, - { - "id": "0431cfd30d7876c1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "avatar", - "discovered_via": "registry.json" - }, - { - "id": "333ea30f1954506d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "badge", - "discovered_via": "registry.json", - "variants": ["default", "secondary", "destructive", "outline"] - }, - { - "id": "efe2444f089a485e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "breadcrumb", - "discovered_via": "registry.json" - }, - { - "id": "0a7a3e198953d1fc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "button-group", - "discovered_via": "registry.json", - "variants": ["horizontal", "vertical"] - }, - { - "id": "58f21138e7471637", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "button", - "discovered_via": "registry.json", - "variants": [ - "default", - "destructive", - "outline", - "secondary", - "ghost", - "link" - ], - "sizes": ["default", "sm", "lg", "icon", "icon-xs", "icon-sm"] - }, - { - "id": "a4885ea9d69e41a3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "calendar", - "discovered_via": "registry.json" - }, - { - "id": "8ce0b357af0795c3", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "card", - "discovered_via": "registry.json" - }, - { - "id": "fbfb10c74df678af", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "carousel", - "discovered_via": "registry.json" - }, - { - "id": "74129d12b38a38dc", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "chart", - "discovered_via": "registry.json" - }, - { - "id": "13735bd47dd8aaf5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "checkbox", - "discovered_via": "registry.json" - }, - { - "id": "22774d3f11a3be63", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "collapsible", - "discovered_via": "registry.json" - }, - { - "id": "16cdb74ba30c1d90", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "command", - "discovered_via": "registry.json" - }, - { - "id": "d4671d867aa81914", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "context-menu", - "discovered_via": "registry.json" - }, - { - "id": "80e28d7e9d7dfb10", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "dialog", - "discovered_via": "registry.json" - }, - { - "id": "f755ac3fcc424ea1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "drawer", - "discovered_via": "registry.json" - }, - { - "id": "1f772aba0ade21d6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "dropdown-menu", - "discovered_via": "registry.json" - }, - { - "id": "2159f8988fcf210d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "form", - "discovered_via": "registry.json" - }, - { - "id": "070a7df1c490f5c0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "hover-card", - "discovered_via": "registry.json" - }, - { - "id": "5a000b51f7025874", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "input-group", - "discovered_via": "registry.json", - "sizes": ["xs", "sm", "icon-xs", "icon-sm"] - }, - { - "id": "50e75f98a13b89e8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "input-otp", - "discovered_via": "registry.json" - }, - { - "id": "27a581643648f096", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "input", - "discovered_via": "registry.json" - }, - { - "id": "6a92e6a05707cad4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "label", - "discovered_via": "registry.json" - }, - { - "id": "3f5ce951ec2755a4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "menubar", - "discovered_via": "registry.json" - }, - { - "id": "ba608373fbf23cce", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "navigation-menu", - "discovered_via": "registry.json" - }, - { - "id": "f5c7cb46fa01e442", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "pagination", - "discovered_via": "registry.json" - }, - { - "id": "6de7fb03fabc400d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "popover", - "discovered_via": "registry.json" - }, - { - "id": "d0c593eb8ad4084f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "progress", - "discovered_via": "registry.json" - }, - { - "id": "be62975f1394e6ff", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "radio-group", - "discovered_via": "registry.json" - }, - { - "id": "9645caec1ec9491e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "resizable", - "discovered_via": "registry.json" - }, - { - "id": "5f9b21232e8a8066", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "scroll-area", - "discovered_via": "registry.json" - }, - { - "id": "cf17878a7f58663b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "select", - "discovered_via": "registry.json" - }, - { - "id": "085fc19879652d91", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "separator", - "discovered_via": "registry.json" - }, - { - "id": "f4064eeeb3884ca6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "sheet", - "discovered_via": "registry.json" - }, - { - "id": "87b0694fe3a9499f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "sidebar", - "discovered_via": "registry.json", - "variants": ["default", "outline"], - "sizes": ["default", "sm", "lg"] - }, - { - "id": "0570886b2bf7ab77", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "skeleton", - "discovered_via": "registry.json" - }, - { - "id": "af98d635e24c253a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "slider", - "discovered_via": "registry.json" - }, - { - "id": "7961b283b03d286c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "sonner", - "discovered_via": "registry.json" - }, - { - "id": "1f3d36ca9a346b48", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "spinner", - "discovered_via": "registry.json" - }, - { - "id": "be951970a77d66e1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "switch", - "discovered_via": "registry.json" - }, - { - "id": "ba4ce3b237361b1b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "table", - "discovered_via": "registry.json" - }, - { - "id": "345efdb74d915ed6", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "tabs", - "discovered_via": "registry.json" - }, - { - "id": "694d8e204bdd6c5d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "textarea", - "discovered_via": "registry.json" - }, - { - "id": "38a5e6bb2cffc90c", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "toggle-group", - "discovered_via": "registry.json" - }, - { - "id": "bf182a9112e8554d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "toggle", - "discovered_via": "registry.json", - "variants": ["default", "outline"], - "sizes": ["default", "sm", "lg"] - }, - { - "id": "d7f1216afd5e5cde", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "tooltip", - "discovered_via": "registry.json" - }, - { - "id": "6793b571e3e9780b", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "agent", - "discovered_via": "registry.json" - }, - { - "id": "8759f8619158f678", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "artifact", - "discovered_via": "registry.json" - }, - { - "id": "6e3f9597896dfd52", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "attachments", - "discovered_via": "registry.json" - }, - { - "id": "75935977758a3be1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "audio-player", - "discovered_via": "registry.json" - }, - { - "id": "bb528a1fe252f6d7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "canvas", - "discovered_via": "registry.json" - }, - { - "id": "b2c0af74cd94d90a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "chain-of-thought", - "discovered_via": "registry.json" - }, - { - "id": "657d96d21d0582da", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "checkpoint", - "discovered_via": "registry.json" - }, - { - "id": "7941f92f7883c8b2", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "code-block", - "discovered_via": "registry.json" - }, - { - "id": "036f33455f4de2dd", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "commit", - "discovered_via": "registry.json" - }, - { - "id": "36d51a73b1f6db11", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "confirmation", - "discovered_via": "registry.json" - }, - { - "id": "3bb44f9468afbbac", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "connection", - "discovered_via": "registry.json" - }, - { - "id": "52f43573e557b417", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "context", - "discovered_via": "registry.json" - }, - { - "id": "ba48935309cafcb4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "controls", - "discovered_via": "registry.json" - }, - { - "id": "161a9b587067b49e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "conversation", - "discovered_via": "registry.json" - }, - { - "id": "89513853a2d97717", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "edge", - "discovered_via": "registry.json" - }, - { - "id": "41622c54dd4965a9", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "environment-variables", - "discovered_via": "registry.json" - }, - { - "id": "ab995ca66746754a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "file-tree", - "discovered_via": "registry.json" - }, - { - "id": "376e2582a06ec73e", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "image", - "discovered_via": "registry.json" - }, - { - "id": "4fd86fe64b97a211", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "inline-citation", - "discovered_via": "registry.json" - }, - { - "id": "37fead59348fa7ae", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "jsx-preview", - "discovered_via": "registry.json" - }, - { - "id": "c817485d020088b0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "message", - "discovered_via": "registry.json" - }, - { - "id": "5a30479915594cc4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "mic-selector", - "discovered_via": "registry.json" - }, - { - "id": "686488567e039191", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "model-selector", - "discovered_via": "registry.json" - }, - { - "id": "a68077a20596305f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "node", - "discovered_via": "registry.json" - }, - { - "id": "2be8f1bd21e615c8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "open-in-chat", - "discovered_via": "registry.json" - }, - { - "id": "3cda4447d444ef2d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "package-info", - "discovered_via": "registry.json" - }, - { - "id": "14dc2b4c9c736023", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "panel", - "discovered_via": "registry.json" - }, - { - "id": "826ce6123a601318", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "persona", - "discovered_via": "registry.json" - }, - { - "id": "528eef1723b0c1f0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "plan", - "discovered_via": "registry.json" - }, - { - "id": "a746aa68c435f69a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "prompt-input", - "discovered_via": "registry.json" - }, - { - "id": "006569c68a95b9d8", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "queue", - "discovered_via": "registry.json" - }, - { - "id": "ca9a01c61e6c4ac4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "reasoning", - "discovered_via": "registry.json" - }, - { - "id": "a5bcf971ed71d2d0", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "sandbox", - "discovered_via": "registry.json" - }, - { - "id": "d6457c65ca1f951a", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "schema-display", - "discovered_via": "registry.json" - }, - { - "id": "0ccb75b51bd72ba5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "shimmer", - "discovered_via": "registry.json" - }, - { - "id": "ad792cdd123a8470", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "snippet", - "discovered_via": "registry.json" - }, - { - "id": "14d45568bf743cda", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "sources", - "discovered_via": "registry.json" - }, - { - "id": "ce2a234fd9564d92", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "speech-input", - "discovered_via": "registry.json" - }, - { - "id": "6048c94cd3783721", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "stack-trace", - "discovered_via": "registry.json" - }, - { - "id": "7b1ca587296e69e1", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "suggestion", - "discovered_via": "registry.json" - }, - { - "id": "d08a52dcd4eabc57", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "task", - "discovered_via": "registry.json" - }, - { - "id": "cbb9ff18df991274", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "terminal", - "discovered_via": "registry.json" - }, - { - "id": "820ee1113b9ab892", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "test-results", - "discovered_via": "registry.json" - }, - { - "id": "08d81406056757b4", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "tool", - "discovered_via": "registry.json" - }, - { - "id": "40b8ee04504e5be7", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "toolbar", - "discovered_via": "registry.json" - }, - { - "id": "8c41d4c1d8b4c07f", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "transcription", - "discovered_via": "registry.json" - }, - { - "id": "9dbe3bfe3aa3457d", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "voice-selector", - "discovered_via": "registry.json" - }, - { - "id": "439b7643cae3b0f5", - "source": { - "target": "github:block/ghost", - "commit": "16f0ab5", - "scanned_at": "2026-04-30T15:20:51Z" - }, - "name": "web-preview", - "discovered_via": "registry.json" - } - ] -} diff --git a/apps/server/directions/ghost/exemplars/article.html b/apps/server/directions/ghost/exemplars/article.html deleted file mode 100644 index 20d4bff..0000000 --- a/apps/server/directions/ghost/exemplars/article.html +++ /dev/null @@ -1,51 +0,0 @@ - - -
-
Finance · 6 min read
-

Tariffs, translated into the grocery bill

-

When the US puts a 25% tariff on imported steel, a Korean-made refrigerator doesn't just get 25% more expensive at the register. The actual pass-through depends on three things: the importer's margin, the retailer's margin, and how much competition pushes back on the price hike.

-
A $1,200 fridge with a 25% tariff might only rise to $1,340 at the register — not $1,500.
-

In 2019, economists tracking the Trump-era tariffs found that about 55% of tariff costs made it through to retail prices. The rest was absorbed by importers trimming margin, manufacturers offering credits, or retailers eating the difference to keep shelf prices competitive. For goods with tight substitutes (grocery staples, cheap electronics), the pass-through is even lower — closer to 30%.

-

What changes fast vs. slowly

-

Anything with a short shelf life — produce, electronics with rapid model cycles — adjusts within weeks. Durable goods like appliances lag by 3 to 6 months as existing pre-tariff inventory clears.

-
diff --git a/apps/server/directions/ghost/exemplars/badge.html b/apps/server/directions/ghost/exemplars/badge.html deleted file mode 100644 index a2f917f..0000000 --- a/apps/server/directions/ghost/exemplars/badge.html +++ /dev/null @@ -1,31 +0,0 @@ - - -
- Active - Draft - Overdue -
diff --git a/apps/server/directions/ghost/exemplars/button.html b/apps/server/directions/ghost/exemplars/button.html deleted file mode 100644 index 2cb0d80..0000000 --- a/apps/server/directions/ghost/exemplars/button.html +++ /dev/null @@ -1,27 +0,0 @@ - - -
- - - -
diff --git a/apps/server/directions/ghost/exemplars/card.html b/apps/server/directions/ghost/exemplars/card.html deleted file mode 100644 index 0fafe76..0000000 --- a/apps/server/directions/ghost/exemplars/card.html +++ /dev/null @@ -1,61 +0,0 @@ - - -
-
-
-
Weekly summary
-

Revenue steady at $284k

-

Up 3.2% week-over-week, driven by mid-market renewals.

-
- -
-
-
-
$284,120
-
Revenue
-
-
-
1,482
-
Orders
-
-
-
$192
-
AOV
-
-
-
diff --git a/apps/server/directions/ghost/exemplars/cell.html b/apps/server/directions/ghost/exemplars/cell.html deleted file mode 100644 index 63b3cfd..0000000 --- a/apps/server/directions/ghost/exemplars/cell.html +++ /dev/null @@ -1,54 +0,0 @@ - - -
    -
  • -
    SC
    -
    -
    Sarah Chen
    -
    Quarterly review scheduled
    -
    -
    -
    $4,280.50
    -
    Mar 14
    -
    -
  • -
  • -
    MJ
    -
    -
    Marcus Johnson
    -
    Onboarding week 2 complete
    -
    -
    -
    $1,105.00
    -
    Mar 12
    -
    -
  • -
diff --git a/apps/server/directions/ghost/exemplars/comparison.html b/apps/server/directions/ghost/exemplars/comparison.html deleted file mode 100644 index 228ac3f..0000000 --- a/apps/server/directions/ghost/exemplars/comparison.html +++ /dev/null @@ -1,107 +0,0 @@ - - -
-
-
-
Option A
-
-

Standing desk

- $380 -
-
    -
  • Reduces static sitting load
  • -
  • Improves posture if you use it
  • -
  • Standing all day has its own problems
  • -
  • Doesn't address your specific back issue
  • -
-
-
-
- Option B - Recommended -
-
-

Ergonomic chair

- $620 -
-
    -
  • Adjustable lumbar targets your lower-back issue
  • -
  • Benefits accrue all day, not just when you remember
  • -
  • Less disruptive to your flow than standing
  • -
  • Higher upfront cost, but single purchase
  • -
-
-
-
diff --git a/apps/server/directions/ghost/exemplars/input.html b/apps/server/directions/ghost/exemplars/input.html deleted file mode 100644 index 2ee148c..0000000 --- a/apps/server/directions/ghost/exemplars/input.html +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/apps/server/directions/ghost/exemplars/tracker.html b/apps/server/directions/ghost/exemplars/tracker.html deleted file mode 100644 index 9eeab7a..0000000 --- a/apps/server/directions/ghost/exemplars/tracker.html +++ /dev/null @@ -1,109 +0,0 @@ - - -
-
Reading · 2026
- -
-
9 / 24
-
books finished — - ahead of pace - by 2 -
-
- -
-
-
- -
-
-
4
-
Fiction
-
-
-
3
-
Essays
-
-
-
2
-
Memoir
-
-
-
diff --git a/apps/server/directions/ghost/meta.json b/apps/server/directions/ghost/meta.json deleted file mode 100644 index 3ba6461..0000000 --- a/apps/server/directions/ghost/meta.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Ghost", - "description": "Monochromatic, magazine-inspired. Pill-rounded interactions, moderate-radius containers, tight editorial typography.", - "overridable": ["color-accent", "color-accent-fg"], - "sourceExpression": { - "id": "ghost-ui", - "commit": "16f0ab5" - } -} diff --git a/apps/server/directions/ghost/prompt.md b/apps/server/directions/ghost/prompt.md deleted file mode 100644 index 79448c6..0000000 --- a/apps/server/directions/ghost/prompt.md +++ /dev/null @@ -1,81 +0,0 @@ -# Ghost UI — Design Direction - -*Source: Ghost UI expression snapshot (commit `16f0ab5`; see `bucket.json` for scanned provenance). Re-sync outside Summon when the portable Ghost expression evolves.* - -This is the design direction for Ghost UI — the visual vocabulary you emit into HTML. Response shape (plan vs article vs comparison vs tracker) is your call based on the user's intent; this document tells you *how* things look once you've picked a shape. - -## Character - -A monochromatic, magazine-inspired design language that treats color as communication rather than decoration. The default palette is entirely achromatic — near-black on white — with hue reserved for semantic states and chart data. Pill-shaped interactive elements contrast with moderately rounded containers, and display typography pushes ultra-tight line-heights (0.85–0.88) with heavy negative tracking for an editorial spread aesthetic. The system ships no bundled typefaces; the host's platform face becomes the brand face. - -## Signature - -- Achromatic by default — primary/accent is the extremity of the gray scale (`#1a1a1a`). Color is opt-in semantic communication, not ambient decoration -- Pill-first radius philosophy — buttons, inputs, and badges fully round to 999px; structural containers (cards, modals) use moderate radii (10–24px). Shape is how users intuit what is tappable versus what is container -- Magazine-scale display typography — headings push ultra-tight line-heights (0.85–0.88) with heavy negative letter-spacing (−0.05em); paired with uppercase label type at 0.12em tracking as "byline voice" -- Layered shadow hierarchy named by role (mini / card / elevated / popover / modal), not by numeric size -- Compact controls inside spacious containers — buttons and inputs sit at 32–40px height; cards breathe at 24px internal padding; sections at 75–100px vertical -- No gradients, no illustrations, no decorative hover effects — motion is functional (fade, scale-in, accordion), never ornamental -- No bundled fonts — system-ui sans, Geist Mono, and a generic serif fallback chain - -## Decisions - -### Color - -- **Default is achromatic.** Grays only — `--color-bg` through `--color-text`. Any chromatic token that appears must carry semantic meaning (danger, success, info, warning) or be data (charts). -- **Accent is the extremity of the gray scale**, not a brand hue. `--color-accent` maps to `#1a1a1a` on light. Use it sparingly — one accent surface per composition. -- **State colors are reserved** — red for danger, green for success, blue for info, yellow for warning. Do not repurpose them for decoration. -- **Off-palette hex literals are drift.** Every color in your output should resolve through a `--color-*` token (or a chart hex, when emitting charts). - -### Shape - -- **Interactive elements are pills.** Buttons, badges, chips, and text inputs use `border-radius: var(--radius-pill)` (999px). This is non-negotiable — it's how users recognize tappable. -- **Structural containers are moderately rounded.** Cards, panels, dialogs, and surfaces use `--radius-lg` (20px) or `--radius-md` (14px). Never pill-round a card. -- **Container radii come from the canonical set** (`--radius-sm` through `--radius-xl`). Avoid arbitrary `border-radius: 13px` — it breaks the shape vocabulary. - -### Typography - -- **Display type is editorial**: large heading (`--text-2xl` or more), weight 700–900, line-height 0.88–0.95, letter-spacing `-0.03em` to `-0.05em`. Tight, confident. -- **Body copy relaxes**: line-height 1.55–1.65, neutral tracking. Comfortable for reading. -- **Use uppercase label type for eyebrows** — small (11–12px), weight 600, letter-spacing 0.12em. Acts as a section kicker. -- **Type sizes come from the ramp** (`--text-xs` through `--text-display`). At most three type sizes per surface — size is not the only hierarchy lever; use weight, color, and tracking too. - -### Rhythm - -- **Compact controls, spacious containers.** Buttons 32–40px tall; cards padded generously (24px internal); sections use tall vertical rhythm. -- **Group tightly, separate generously.** Items within a group use `--space-2` to `--space-3`; groups within a section use `--space-5` to `--space-6`. -- **Bottom of a surface should feel lighter** than the top — fewer elements, more room. -- **Spacing comes from the 4px-base scale** (4 / 8 / 12 / 16 / 24 / 32 / 52 / 75 / 100). Off-scale values like `padding: 13px` break layout rhythm. - -### Surface hierarchy - -- **Use `--color-surface-muted` as layering**, not decoration. Alternate rows, secondary blocks, or contextual backgrounds. -- **Borders are optional.** Either a thin border OR a muted background — rarely both on the same element. -- **Name surfaces by intent**: `--color-bg` is page; `--color-surface` is an elevated panel/card; `--color-surface-muted` is a secondary or disabled surface. - -### Elevation - -- **Shadows are named by role, not size.** `--shadow-mini` for buttons and cards at rest; `--shadow-card` shares that tier; `--shadow-elevated` for raised panels; `--shadow-popover` for floating layers; `--shadow-modal` for dialogs. -- **Shadows cue elevation, not decoration.** A hover that lifts a card is fine; ambient shadows on every container are not. -- **Don't invent shadow values inline.** Reach for a role token, or omit shadow entirely. - -### Motion - -- **Animations are functional, never decorative.** Reveals only — accordion expand, fade-in, scale-in, word-reveal entrance, route transitions. -- **No hover ornaments.** No `transition: all`, no looping spinners, no bouncing chevrons. The editorial tone stays serious. -- **One easing, three durations** — fast (~0.15s), normal (~0.2s), slow (~0.4s) on a single spring curve. Don't mix durations within a surface. - -### Focus - -- **A single focus rule.** A 1px ring in `--color-border-strong` at half opacity, applied uniformly to buttons, inputs, and badges. Don't replace it with ad-hoc outlines, color shifts, or border swaps. - -### Fonts - -- **No bundled typefaces.** Use the declared `--font-sans` / `--font-mono` / `--font-serif` stacks — they're system fallback chains; the host's platform face is the brand face. -- **Don't `@import` web fonts or declare `@font-face`.** A foreign typeface breaks the language. - -## Voice guidance for generated content - -- Be specific. "Sarah Chen", "$4,280.50", "Mar 14" — not "user name", "amount", "date". -- Be direct. No hedging, no "here's your…" preambles. -- The editorial eyebrow-over-tight-headline pattern is one tool, not the default. Use it when the response actually leads with a declarative headline (a recommendation, a status, a launch). For long-form explanations, lead with body copy. For trackers, lead with the big number. For comparisons, skip a top headline entirely and let the compared options do the talking. diff --git a/apps/server/directions/ghost/tokens.css b/apps/server/directions/ghost/tokens.css deleted file mode 100644 index bdc7a63..0000000 --- a/apps/server/directions/ghost/tokens.css +++ /dev/null @@ -1,82 +0,0 @@ -/* Ghost UI tokens (portable expression snapshot, ghost commit 16f0ab5; see bucket.json). - * Remapped into Summon's --color-* / --space-* / --radius-* naming so the sandbox - * stays source-agnostic — swap directions, swap this file, every exemplar and - * LLM-emitted class using var(--...) re-themes automatically. - */ -:root { - color-scheme: light; - - /* Palette — monochromatic by design, hue reserved for state */ - --color-bg: #ffffff; - --color-surface: #ffffff; - --color-surface-muted: #f5f5f5; - --color-border: #e8e8e8; - --color-border-input: #e5e5e5; - --color-border-strong: #1a1a1a; - --color-text: #1a1a1a; - --color-text-muted: #999999; - --color-text-alt: #666666; - --color-accent: #1a1a1a; - --color-accent-fg: #ffffff; - --color-danger: #f94b4b; - --color-success: #91cb80; - --color-info: #5c98f9; - --color-warning: #fbcd44; - - /* Spacing — Ghost fingerprint scale [2,4,6,8,12,16,20,24,32,36,40,52,75,100] */ - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 24px; - --space-6: 32px; - --space-8: 52px; - --space-10: 75px; - --space-12: 100px; - - /* Radii — pill for interactive, moderate for structural */ - --radius-pill: 999px; - --radius-sm: 10px; - --radius-md: 14px; - --radius-lg: 20px; - --radius-xl: 24px; - - /* Typography — editorial scale, system fallback chain */ - --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace; - --font-serif: "Iowan Old Style", "Palatino Linotype", Georgia, serif; - --text-xs: 11px; - --text-sm: 13px; - --text-md: 15px; - --text-lg: 20px; - --text-xl: 28px; - --text-2xl: 40px; - --text-3xl: 64px; - --text-display: clamp(48px, 7vw, 80px); - - /* Editorial typographic tuning */ - --tracking-label: 0.12em; /* uppercase eyebrow */ - --tracking-tight: -0.03em; /* section headings */ - --tracking-display: -0.05em; /* hero headings */ - --leading-display: 0.88; - --leading-section: 0.95; - --leading-body: 1.55; - --leading-reading: 1.65; - - /* Shadows — named by role, layered */ - --shadow-mini: 0 2px 8px rgba(76, 76, 76, 0.15); - --shadow-card: 0 2px 8px rgba(76, 76, 76, 0.15); - --shadow-elevated: 0 3px 12px rgba(76, 76, 76, 0.22); - --shadow-popover: 0 8px 30px rgba(0, 0, 0, 0.12); - --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.2); -} - -html, body { - margin: 0; - background: var(--color-bg); - color: var(--color-text); - font-family: var(--font-sans); - font-size: var(--text-md); - line-height: var(--leading-body); - -webkit-font-smoothing: antialiased; -} diff --git a/apps/server/package.json b/apps/server/package.json index 5557f2d..fd10a47 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@anarchitecture/ghost": "^0.2.0", + "@anarchitecture/ghost": "0.9.0", "@anthropic-ai/sdk": "^0.88.0", "cors": "^2.8.5", "express": "^4.21.2", diff --git a/apps/server/src/directions-loader.test.ts b/apps/server/src/directions-loader.test.ts index b397f12..5a44250 100644 --- a/apps/server/src/directions-loader.test.ts +++ b/apps/server/src/directions-loader.test.ts @@ -12,15 +12,14 @@ import { defaultDirectionId, loadDirections } from './directions-loader.js'; const here = dirname(fileURLToPath(import.meta.url)); const repoRoot = join(here, '..', '..', '..'); -test('bundled public directions load in Ghost-first order', () => { +test('bundled public directions load with Workbench as default', () => { const directions = loadDirections(); assert.deepEqual(directions.map((direction) => direction.id), [ - 'ghost', - 'pulse', 'workbench', + 'pulse', ]); - assert.equal(defaultDirectionId(directions), 'ghost'); + assert.equal(defaultDirectionId(directions), 'workbench'); }); test('public source has no bundled product-design references outside Ghost', () => { @@ -66,7 +65,6 @@ function publicSourceFiles(): string[] { function collectFiles(path: string): string[] { const stat = statSync(path); if (stat.isFile()) { - if (path.endsWith('apps/server/directions/ghost/bucket.json')) return []; return [path]; } if (!stat.isDirectory()) return []; diff --git a/apps/server/src/directions-loader.ts b/apps/server/src/directions-loader.ts index 9c1e2ce..c0c16a6 100644 --- a/apps/server/src/directions-loader.ts +++ b/apps/server/src/directions-loader.ts @@ -7,7 +7,7 @@ import { type DirectionOpts, } from '@anarchitecture/summon/engine'; -export const PREFERRED_DEFAULT_DIRECTION_ID = 'ghost'; +export const PREFERRED_DEFAULT_DIRECTION_ID = 'workbench'; export interface DirectionExemplar { name: string; diff --git a/apps/server/src/generate-route.test.ts b/apps/server/src/generate-route.test.ts index 480c237..dddfc9e 100644 --- a/apps/server/src/generate-route.test.ts +++ b/apps/server/src/generate-route.test.ts @@ -55,7 +55,27 @@ test('api generate sends narrowed contract and stream meta shape through package res.end(); return; } - anthropicRequests.push(JSON.parse(await readBody(req))); + const request = JSON.parse(await readBody(req)); + anthropicRequests.push(request); + const systemText = Array.isArray(request.system) + ? request.system.map((block: { text?: unknown }) => typeof block.text === 'string' ? block.text : '').join('\n') + : ''; + const generatedText = systemText.includes('Experimental HTML node patches') + ? [ + '{"op":"set","path":"/screen","value":{"sections":["hero"]}}\n', + '{"op":"add","path":"/section/hero/node/root","html":"
"}\n', + '{"op":"add","path":"/section/hero/node/headline","parent":"root","html":"

Dinner finder

"}\n', + ] + : systemText.includes('Experimental block fragments') + ? [ + '{"op":"set","path":"/screen","value":{"sections":["hero"]}}\n', + '{"op":"set","path":"/section/hero","value":{"blocks":["headline"]}}\n', + '{"op":"add","path":"/section/hero/block/headline","html":"

Dinner finder

Ready.

"}\n', + ] + : [ + '{"op":"set","path":"/screen","value":{"sections":["hero"]}}\n', + '{"op":"add","path":"/section/hero","html":"

Dinner finder

Ready.

"}\n', + ]; res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', @@ -68,7 +88,7 @@ test('api generate sends narrowed contract and stream meta shape through package id: 'msg_test', type: 'message', role: 'assistant', - model: 'claude-sonnet-4-6', + model: 'claude-opus-4-8', content: [], stop_reason: null, stop_sequence: null, @@ -85,7 +105,7 @@ test('api generate sends narrowed contract and stream meta shape through package index: 0, delta: { type: 'text_delta', - text: '{"op":"set","path":"/screen","value":{"sections":["hero"]}}\n', + text: generatedText[0], }, }), sse('content_block_delta', { @@ -93,7 +113,7 @@ test('api generate sends narrowed contract and stream meta shape through package index: 0, delta: { type: 'text_delta', - text: '{"op":"add","path":"/section/hero","html":"

Dinner finder

Ready.

"}\n', + text: generatedText.slice(1).join(''), }, }), sse('content_block_stop', { @@ -128,7 +148,7 @@ test('api generate sends narrowed contract and stream meta shape through package OPENAI_API_KEY: '', GEMINI_API_KEY: '', GOOGLE_API_KEY: '', - SUMMON_INFER_CAPABILITIES: '0', + SUMMON_AGENT_INTENT_MODEL: '0', SUMMON_INFER_SHAPE: '0', }, stdio: ['ignore', 'pipe', 'pipe'], @@ -157,7 +177,7 @@ test('api generate sends narrowed contract and stream meta shape through package assert.equal(anthropicRequests.length, 1); const request = anthropicRequests[0] as { model?: string; system?: Array<{ text?: string }>; stream?: boolean }; assert.equal(request.stream, true); - assert.equal(request.model, 'claude-sonnet-4-6'); + assert.equal(request.model, 'claude-opus-4-8'); const systemText = request.system?.map((block) => block.text ?? '').join('\n') ?? ''; assert.match(systemText, /Search host-owned dinner data/); assert.match(systemText, /host-resource/); @@ -274,6 +294,8 @@ test('api generate sends narrowed contract and stream meta shape through package ]); const agentIntent = agentLines[1] as Extract; assert.equal((agentIntent.value as { interaction?: unknown }).interaction, 'search'); + const agentResolution = agentLines[2] as Extract; + assert.equal((agentResolution.value as { intentSource?: unknown }).intentSource, 'deterministic'); const agentPolicy = agentLines[3] as Extract; assert.deepEqual(agentPolicy.value, { tier: 'declarative', @@ -282,6 +304,79 @@ test('api generate sends narrowed contract and stream meta shape through package components: [], persistence: 'replayable', }); + + const blockResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + prompt: 'build a dinner finder in blocks', + mode: 'interactive', + capabilities: searchCapability, + surfacePlan, + surfaceCeiling, + scriptPolicy: 'forbid', + fragmentMode: 'block-v0', + }), + }); + const blockBody = await blockResponse.text(); + assert.equal(blockResponse.status, 200, blockBody); + + assert.equal(anthropicRequests.length, 4); + const blockRequest = anthropicRequests[3] as { system?: Array<{ text?: string }>; stream?: boolean }; + assert.equal(blockRequest.stream, true); + const blockSystemText = blockRequest.system?.map((block) => block.text ?? '').join('\n') ?? ''; + assert.match(blockSystemText, /Experimental block fragments/); + assert.match(blockSystemText, /add \/section\/\/block\//); + + const blockLines = blockBody + .trim() + .split(/\n/) + .filter(Boolean) + .map((raw) => JSON.parse(raw) as ProtocolLine); + assert.equal(blockLines.some((line) => line.path === '/experimental-fragments'), true); + assert.equal(blockLines.some((line) => line.path === '/section/hero'), true); + assert.equal(blockLines.some((line) => line.path === '/section/hero/block/headline'), true); + const blockSummary = blockLines.find((line) => line.path === '/stream-graph-summary') as + | Extract + | undefined; + assert.match(JSON.stringify(blockSummary?.value), /declaredBlockCount/); + + const nodeResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + prompt: 'build a dinner finder in html nodes', + mode: 'interactive', + capabilities: searchCapability, + surfacePlan, + surfaceCeiling, + scriptPolicy: 'forbid', + fragmentMode: 'html-node-v0', + }), + }); + const nodeBody = await nodeResponse.text(); + assert.equal(nodeResponse.status, 200, nodeBody); + + assert.equal(anthropicRequests.length, 5); + const nodeRequest = anthropicRequests[4] as { system?: Array<{ text?: string }>; stream?: boolean }; + assert.equal(nodeRequest.stream, true); + const nodeSystemText = nodeRequest.system?.map((block) => block.text ?? '').join('\n') ?? ''; + assert.match(nodeSystemText, /Experimental HTML node patches/); + assert.match(nodeSystemText, /add \/section\/\/node\//); + + const nodeLines = nodeBody + .trim() + .split(/\n/) + .filter(Boolean) + .map((raw) => JSON.parse(raw) as ProtocolLine); + assert.equal(nodeLines.some((line) => line.path === '/experimental-fragments'), true); + assert.equal(nodeLines.some((line) => line.path === '/section/hero/node/root'), true); + assert.equal(nodeLines.some((line) => line.path === '/section/hero/node/headline'), true); + const nodeSummary = nodeLines.find((line) => line.path === '/stream-graph-summary') as + | Extract + | undefined; + assert.match(JSON.stringify(nodeSummary?.value), /presentNodeCount/); + const ghostResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -298,31 +393,9 @@ test('api generate sends narrowed contract and stream meta shape through package }), }); const ghostBody = await ghostResponse.text(); - assert.equal(ghostResponse.status, 200, ghostBody); - - assert.equal(anthropicRequests.length, 4); - const ghostRequest = anthropicRequests[3] as { system?: Array<{ text?: string }>; stream?: boolean }; - const ghostSystemText = ghostRequest.system?.map((block) => block.text ?? '').join('\n') ?? ''; - assert.match(ghostSystemText, /Checkout product experience/); - - const ghostLines = ghostBody - .trim() - .split(/\n/) - .filter(Boolean) - .map((raw) => JSON.parse(raw) as ProtocolLine); - assert.deepEqual(ghostLines.slice(0, 4).map((line) => `${line.op} ${line.path}`), [ - 'meta /ghost-context', - 'meta /ghost-token-source', - 'meta /surface-plan', - 'meta /status', - ]); - const ghostContext = ghostLines.find((line) => line.path === '/ghost-context') as Extract; - assert.equal((ghostContext.value as { source?: unknown }).source, 'resolved-context'); - assert.equal((ghostContext.value as { product?: unknown }).product, 'Checkout'); - const ghostTokenSource = ghostLines.find((line) => line.path === '/ghost-token-source') as Extract; - assert.equal((ghostTokenSource.value as { kind?: unknown }).kind, 'base-direction'); - const ghostReviewPacket = ghostLines.find((line) => line.path === '/ghost-review-packet') as Extract; - assert.equal((ghostReviewPacket.value as { source?: unknown }).source, 'resolved-context'); + assert.equal(ghostResponse.status, 400); + assert.match(ghostBody, /resolved-context is no longer supported/); + assert.equal(anthropicRequests.length, 5); const ghostOverrideResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, { method: 'POST', @@ -338,11 +411,11 @@ test('api generate sends narrowed contract and stream meta shape through package }); const ghostOverrideBody = await ghostOverrideResponse.text(); assert.equal(ghostOverrideResponse.status, 400); - assert.match(ghostOverrideBody, /tokenOverrides are not supported with Ghost product memory/); - assert.equal(anthropicRequests.length, 4); + assert.match(ghostOverrideBody, /resolved-context is no longer supported/); + assert.equal(anthropicRequests.length, 5); }); -test('api generate emits compact Ghost capsule for root contexts', async (t) => { +test('api generate emits Ghost fingerprint context for root contexts', async (t) => { const root = await makeRouteGhostFixture(); t.after(async () => { await rm(root, { recursive: true, force: true }); @@ -368,7 +441,7 @@ test('api generate emits compact Ghost capsule for root contexts', async (t) => id: 'msg_ghost', type: 'message', role: 'assistant', - model: 'claude-sonnet-4-6', + model: 'claude-opus-4-8', content: [], stop_reason: null, stop_sequence: null, @@ -416,8 +489,7 @@ test('api generate emits compact Ghost capsule for root contexts', async (t) => GEMINI_API_KEY: '', GOOGLE_API_KEY: '', SUMMON_GHOST_ROOTS: `checkout=${root}`, - SUMMON_GHOST_CONTEXT_MODE: '', - SUMMON_INFER_CAPABILITIES: '0', + SUMMON_AGENT_INTENT_MODEL: '0', SUMMON_INFER_SHAPE: '0', }, stdio: ['ignore', 'pipe', 'pipe'], @@ -447,56 +519,75 @@ test('api generate emits compact Ghost capsule for root contexts', async (t) => const request = anthropicRequests[0] as { system?: Array<{ text?: string }>; stream?: boolean }; assert.equal(request.stream, true); const systemText = request.system?.map((block) => block.text ?? '').join('\n') ?? ''; - assert.match(systemText, /Ghost Capsule/); + assert.match(systemText, /Ghost Relay Brief/); + assert.match(systemText, /Identity Capsule/); + assert.match(systemText, /Summon Surface Brief/); + assert.match(systemText, /product design direction package/); assert.match(systemText, /Status surfaces must foreground current state/); assert.match(systemText, /Surfaces are compact/); - assert.doesNotMatch(systemText, /## Manifest/); - assert.doesNotMatch(systemText, /```yaml/); const lines = body .trim() .split(/\n/) .filter(Boolean) .map((raw) => JSON.parse(raw) as ProtocolLine); - assert.deepEqual(lines.slice(0, 5).map((line) => `${line.op} ${line.path}`), [ + assert.deepEqual(lines.slice(0, 8).map((line) => `${line.op} ${line.path}`), [ 'meta /ghost-context', 'meta /ghost-token-source', - 'meta /ghost-capsule', + 'meta /agent-intent', + 'meta /agent-policy-resolution', + 'meta /surface-policy', 'meta /surface-plan', + 'meta /surface-contract', 'meta /status', ]); + const ghostAgentResolution = lines[3] as Extract; + assert.equal((ghostAgentResolution.value as { intentSource?: unknown }).intentSource, 'deterministic'); - const capsuleLine = lines.find((line) => line.path === '/ghost-capsule') as Extract; - const capsule = capsuleLine.value as { - mode?: unknown; - prompt?: unknown; - promptChars?: number; - budgetChars?: number; - selectedRefs?: { - principles?: string[]; - experienceContracts?: string[]; - patterns?: string[]; - }; + const ghostContext = lines.find((line) => line.path === '/ghost-context') as Extract; + const contextMeta = ghostContext.value as { + source?: unknown; + product?: unknown; + provenance?: { merge?: unknown; layers?: Array<{ relativeRoot?: unknown; memoryDir?: unknown; dir?: unknown }> }; }; - assert.equal(capsule.mode, 'capsule'); - assert.equal(capsule.prompt, undefined); - assert.ok((capsule.promptChars ?? 0) > 0); - assert.ok((capsule.promptChars ?? 0) <= (capsule.budgetChars ?? 0)); - assert.deepEqual(capsule.selectedRefs?.principles, ['calm-density']); - assert.deepEqual(capsule.selectedRefs?.experienceContracts, ['queue-trust']); - assert.deepEqual(capsule.selectedRefs?.patterns, ['measured-surfaces']); - + assert.equal(contextMeta.source, 'root'); + assert.equal(contextMeta.product, 'Checkout'); + assert.equal(contextMeta.provenance?.merge, 'child-wins-by-id'); + assert.deepEqual(contextMeta.provenance?.layers, [ + { relativeRoot: '.', memoryDir: '.ghost', dir: '.ghost' }, + ]); const ghostReviewPacket = lines.find((line) => line.path === '/ghost-review-packet') as Extract; const reviewPacket = ghostReviewPacket.value as { source?: unknown; - memoryProvenance?: { merge?: unknown }; + fingerprintProvenance?: { merge?: unknown; layers?: unknown }; sections?: Array<{ id?: unknown; html?: unknown }>; }; assert.equal(reviewPacket.source, 'root'); - assert.equal(reviewPacket.memoryProvenance?.merge, 'child-wins-by-id'); + assert.equal(reviewPacket.fingerprintProvenance?.merge, 'child-wins-by-id'); + assert.deepEqual(reviewPacket.fingerprintProvenance?.layers, [ + { relativeRoot: '.', memoryDir: '.ghost', dir: '.ghost' }, + ]); assert.deepEqual(reviewPacket.sections, [ { id: 'hero', html: '

Checkout queue

' }, ]); + + const overrideResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + prompt: 'build checkout queue status', + mode: 'static', + ghost: { + rootId: 'checkout', + targetPath: '.', + }, + tokenOverrides: { 'color-accent': 'red' }, + }), + }); + const overrideBody = await overrideResponse.text(); + assert.equal(overrideResponse.status, 400); + assert.match(overrideBody, /tokenOverrides are not supported with Ghost fingerprints/); + assert.equal(anthropicRequests.length, 1); }); test('api generate forwards Anthropic model overrides and speed options', async (t) => { @@ -582,7 +673,7 @@ test('api generate forwards Anthropic model overrides and speed options', async OPENAI_API_KEY: '', GEMINI_API_KEY: '', GOOGLE_API_KEY: '', - SUMMON_INFER_CAPABILITIES: '0', + SUMMON_AGENT_INTENT_MODEL: '0', }, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -706,7 +797,7 @@ test('api generate can stream with OpenAI provider', async (t) => { OPENAI_BASE_URL: `http://127.0.0.1:${openAIPort}/v1`, GEMINI_API_KEY: '', GOOGLE_API_KEY: '', - SUMMON_INFER_CAPABILITIES: '0', + SUMMON_AGENT_INTENT_MODEL: '0', SUMMON_INFER_SHAPE: '0', }, stdio: ['ignore', 'pipe', 'pipe'], @@ -843,7 +934,7 @@ test('api generate can stream with Gemini provider', async (t) => { GEMINI_API_KEY: 'test-gemini-key', GOOGLE_API_KEY: '', GEMINI_BASE_URL: `http://127.0.0.1:${geminiPort}`, - SUMMON_INFER_CAPABILITIES: '0', + SUMMON_AGENT_INTENT_MODEL: '0', SUMMON_INFER_SHAPE: '0', }, stdio: ['ignore', 'pipe', 'pipe'], @@ -917,8 +1008,7 @@ id: checkout ); await writeFile( join(root, '.ghost', 'fingerprint', 'prose.yml'), - `schema: ghost.fingerprint-prose/v1 -summary: + `summary: product: Checkout tone: [quiet, exacting workflows] situations: @@ -927,13 +1017,11 @@ situations: user_intent: Show the current checkout queue state. product_obligation: Keep operator status legible before secondary detail. surface_type: dashboard - paths: [.] - principles: [principle:calm-density] - experience_contracts: [experience_contract:queue-trust] - patterns: [pattern:measured-surfaces] + principles: [prose.principle:calm-density] + experience_contracts: [prose.experience_contract:queue-trust] + patterns: [composition.pattern:measured-surfaces] principles: - id: calm-density - status: accepted principle: Preserve quiet density and clear hierarchy. applies_to: paths: [.] @@ -943,7 +1031,6 @@ principles: check_refs: [check:no-rainbow] experience_contracts: - id: queue-trust - status: accepted contract: Status surfaces must foreground current state. obligations: - Show current queue state before secondary context. @@ -952,8 +1039,7 @@ experience_contracts: ); await writeFile( join(root, '.ghost', 'fingerprint', 'inventory.yml'), - `schema: ghost.fingerprint-inventory/v1 -topology: + `topology: scopes: - id: app paths: [.] @@ -965,11 +1051,9 @@ building_blocks: ); await writeFile( join(root, '.ghost', 'fingerprint', 'composition.yml'), - `schema: ghost.fingerprint-composition/v1 -patterns: + `patterns: - id: measured-surfaces - kind: composition - status: accepted + kind: structure pattern: Surfaces are compact, rectangular, and information-first. guidance: - Use one clear status block before supporting details. @@ -984,9 +1068,19 @@ patterns: id: checkout checks: - id: no-rainbow - active: true - summary: Avoid rainbow decorative color. - pattern: rainbow + title: Avoid rainbow decorative color + status: active + severity: serious + applies_to: + paths: [.] + detector: + type: forbidden-regex + pattern: rainbow + evidence: + support: 1 + observed_count: 1 + examples: + - checkout fixture avoids rainbow decorative color `, ); await writeFile( diff --git a/apps/server/src/ghost-adapter.test.ts b/apps/server/src/ghost-adapter.test.ts index f6559d1..79f795f 100644 --- a/apps/server/src/ghost-adapter.test.ts +++ b/apps/server/src/ghost-adapter.test.ts @@ -22,7 +22,7 @@ afterEach(async () => { }); describe('Ghost adapter', () => { - it('parses trusted roots and rejects unsafe request paths', async () => { + it('parses trusted roots and rejects unsafe or unsupported request paths', async () => { const root = await makeGhostFixture(); const roots = parseGhostRoots(`checkout=${root}`); @@ -43,6 +43,10 @@ describe('Ghost adapter', () => { ok: false, error: 'ghost.targetPath must not contain path traversal segments', }); + assert.deepEqual(parseGhostRequest({ source: 'resolved-context', prompt: 'legacy' }, roots), { + ok: false, + error: 'ghost.source must be "root"; resolved-context is no longer supported', + }); const parsed = parseGhostRequest({ rootId: 'checkout' }, roots); assert.equal(parsed.ok, true); @@ -68,16 +72,13 @@ describe('Ghost adapter', () => { memoryDir: '.ghost', baseDirectionId: 'ghost', }); - const explicitRoot = parseGhostRequest({ source: 'root', rootId: 'checkout' }, roots); - assert.equal(explicitRoot.ok, true); - assert.equal(explicitRoot.ok ? explicitRoot.request?.source : null, 'root'); assert.deepEqual(parseGhostRequest({ rootId: 'checkout', baseDirectionId: '../ghost' }, roots), { ok: false, error: 'ghost.baseDirectionId must be a valid direction id', }); }); - it('resolves a Ghost stack into prompt intent and valid token CSS', async () => { + it('resolves Ghost relay context into prompt intent and valid token CSS', async () => { const root = await makeGhostFixture({ tokenCss: await readDefaultTokensCss() }); const roots = parseGhostRoots(`checkout=${root}`); const parsed = parseGhostRequest({ rootId: 'checkout' }, roots); @@ -87,76 +88,29 @@ describe('Ghost adapter', () => { const ctx = await resolveGhostContext(parsed.request, roots); assert.equal(ctx.source, 'root'); - if (ctx.source !== 'root') assert.fail('expected root context'); + assert.equal(ctx.relay.schema, 'ghost.relay.gather/v1'); + assert.equal(ctx.relay.source.kind, 'stack'); + assert.equal(ctx.relay.source.repoRoot, resolve(root)); + assert.equal(ctx.relay.source.fingerprintDir, '.ghost'); + assert.deepEqual( + ctx.relay.source.provenance.layers.map((layer: { relative_root: string }) => layer.relative_root), + ['.'], + ); assert.equal(ctx.tokenSource.kind, 'ghost-config'); assert.equal(ctx.tokenSource.source, 'tokens.css'); assert.equal(ctx.tokenSource.css, await readDefaultTokensCss()); assert.equal(ctx.product, 'Test Product'); - assert.ok(ctx.capsule); - assert.equal(ctx.capsule.prompt, ctx.prompt); - assert.equal(ctx.capsule.mode, 'capsule'); - assert.ok(ctx.prompt.length <= 4000); - assert.match(ctx.prompt, /Test Product/); - assert.match(ctx.prompt, /Ghost Capsule/); + assert.match(ctx.prompt, /# Ghost Relay Brief/); + assert.match(ctx.prompt, /## Identity Capsule/); + assert.match(ctx.prompt, /Product: Test Product/); assert.match(ctx.prompt, /Preserve quiet density/); assert.match(ctx.prompt, /Status surfaces must foreground current state/); assert.match(ctx.prompt, /Surfaces are compact/); assert.match(ctx.prompt, /exacting workflows/); - assert.match(ctx.prompt, /Human-approved test intent/); - assert.doesNotMatch(ctx.prompt, /## Manifest/); - assert.doesNotMatch(ctx.prompt, /```yaml/); - assert.deepEqual(ctx.capsule.selectedRefs.principles, ['calm-density']); - assert.deepEqual(ctx.capsule.selectedRefs.experienceContracts, ['queue-trust']); - assert.deepEqual(ctx.capsule.selectedRefs.patterns, ['measured-surfaces']); - assert.deepEqual(ctx.capsule.selectedRefs.checks, ['no-rainbow']); - }); - - it('preserves raw Ghost prompt behavior when requested', async () => { - const previousMode = process.env.SUMMON_GHOST_CONTEXT_MODE; - process.env.SUMMON_GHOST_CONTEXT_MODE = 'raw'; - try { - const root = await makeGhostFixture(); - const roots = parseGhostRoots(`checkout=${root}`); - const parsed = parseGhostRequest({ rootId: 'checkout' }, roots); - assert.equal(parsed.ok, true); - if (!parsed.ok || !parsed.request) assert.fail('expected valid Ghost request'); - - const ctx = await resolveGhostContext(parsed.request, roots); - - assert.equal(ctx.source, 'root'); - if (ctx.source !== 'root') assert.fail('expected root context'); - assert.equal(ctx.capsule, undefined); - assert.match(ctx.prompt, /## Manifest/); - assert.match(ctx.prompt, /```yaml/); - assert.match(ctx.prompt, /fingerprint-prose/); - } finally { - if (previousMode === undefined) { - delete process.env.SUMMON_GHOST_CONTEXT_MODE; - } else { - process.env.SUMMON_GHOST_CONTEXT_MODE = previousMode; - } - } + assert.match(ctx.prompt, /fingerprint\/memory\/intent\.md/); }); - it('bridges legacy fingerprint.yml roots', async () => { - const root = await makeLegacyGhostFixture(); - const roots = parseGhostRoots(`checkout=${root}`); - const parsed = parseGhostRequest({ rootId: 'checkout' }, roots); - assert.equal(parsed.ok, true); - if (!parsed.ok || !parsed.request) assert.fail('expected valid Ghost request'); - - const ctx = await resolveGhostContext(parsed.request, roots); - - assert.equal(ctx.source, 'root'); - if (ctx.source !== 'root') assert.fail('expected root context'); - assert.equal(ctx.product, 'Test Product'); - assert.ok(ctx.capsule); - assert.match(ctx.prompt, /Ghost Capsule/); - assert.match(ctx.prompt, /Preserve quiet density/); - assert.doesNotMatch(ctx.prompt, /```yaml/); - }); - - it('keeps large Ghost capsules under the hard static budget', async () => { + it('appends a Summon surface brief without recompiling the fingerprint', async () => { const root = await makeGhostFixture({ large: true }); const roots = parseGhostRoots(`checkout=${root}`); const parsed = parseGhostRequest({ rootId: 'checkout' }, roots); @@ -164,9 +118,6 @@ describe('Ghost adapter', () => { if (!parsed.ok || !parsed.request) assert.fail('expected valid Ghost request'); const ctx = await resolveGhostContext(parsed.request, roots); - - assert.equal(ctx.source, 'root'); - if (ctx.source !== 'root') assert.fail('expected root context'); const prepared = prepareGhostSurfacePrompt(ctx, { userPrompt: 'show checkout queue status', mode: 'static', @@ -179,12 +130,14 @@ describe('Ghost adapter', () => { }, shape: 'card', }); + assert.equal(prepared.source, 'root'); - if (prepared.source !== 'root') assert.fail('expected root context'); - assert.ok(prepared.capsule); - assert.equal(prepared.capsule.budgetChars, 4000); - assert.ok(prepared.prompt.length <= prepared.capsule.budgetChars); - assert.doesNotMatch(prepared.prompt, /```yaml/); + assert.match(prepared.prompt, /# Ghost Relay Brief/); + assert.match(prepared.prompt, /## Summon Surface Brief/); + assert.match(prepared.prompt, /Surface plan: purpose=inform; runtime=static; data=embedded; authority=none; persistence=replayable/); + assert.match(prepared.prompt, /The agent broker controls host authority and capabilities/); + assert.match(prepared.prompt, /Compose from the fingerprint prose, inventory, and composition layers/); + assert.match(prepared.prompt, /Preserve quiet density/); }); it('falls back to Summon default tokens when Ghost token CSS is missing or invalid', async () => { @@ -196,8 +149,6 @@ describe('Ghost adapter', () => { const ctx = await resolveGhostContext(parsed.request, roots); - assert.equal(ctx.source, 'root'); - if (ctx.source !== 'root') assert.fail('expected root context'); assert.equal(ctx.tokenSource.kind, 'summon-default'); assert.equal(ctx.tokenSource.source, '@anarchitecture/summon/tokens.css'); assert.match(ctx.tokenSource.css, /--color-bg:/); @@ -217,24 +168,20 @@ describe('Ghost adapter', () => { tokensCss: baseTokens, }); - assert.equal(ctx.source, 'root'); - if (ctx.source !== 'root') assert.fail('expected root context'); assert.equal(ctx.baseDirectionId, 'ghost'); assert.equal(ctx.tokenSource.kind, 'base-direction'); assert.equal(ctx.tokenSource.source, 'direction:ghost/tokens.css'); assert.equal(ctx.tokenSource.css, baseTokens); - assert.ok(ctx.tokenSource.warnings.some((warning) => warning.includes('using the base Summon direction tokens'))); + assert.ok(ctx.tokenSource.warnings.some((warning) => warning.includes('using the fallback Summon direction tokens'))); }); - it('builds review packet metadata from accepted protocol lines', async () => { + it('builds review packet metadata from relay context and accepted protocol lines', async () => { const root = await makeGhostFixture(); const roots = parseGhostRoots(`checkout=${root}`); const parsed = parseGhostRequest({ rootId: 'checkout' }, roots); assert.equal(parsed.ok, true); if (!parsed.ok || !parsed.request) assert.fail('expected valid Ghost request'); const ctx = await resolveGhostContext(parsed.request, roots); - assert.equal(ctx.source, 'root'); - if (ctx.source !== 'root') assert.fail('expected root context'); const packet = buildGhostReviewPacket({ context: ctx, @@ -249,16 +196,17 @@ describe('Ghost adapter', () => { ], }); - assert.equal(packet.schema, 'summon.ghost-generation/v1'); + assert.equal(packet.schema, 'summon.ghost-fingerprint-generation/v1'); assert.equal(packet.source, 'root'); assert.equal(packet.rootId, 'checkout'); assert.equal(packet.product, 'Test Product'); assert.equal(packet.baseDirectionId, null); assert.equal(packet.styleSource, 'summon-default'); - assert.deepEqual(packet.memoryProvenance, { - merge: 'child-wins-by-id', - layers: [{ relativeRoot: '.', memoryDir: '.ghost' }], - }); + assert.equal(packet.fingerprintProvenance.merge, 'child-wins-by-id'); + assert.deepEqual( + packet.fingerprintProvenance.layers.map(({ relativeRoot, memoryDir, dir }) => ({ relativeRoot, memoryDir, dir })), + [{ relativeRoot: '.', memoryDir: '.ghost', dir: '.ghost' }], + ); assert.deepEqual(packet.declaredSections, ['header', 'content']); assert.deepEqual(packet.sections, [ { id: 'header', html: '

Queue

' }, @@ -267,86 +215,6 @@ describe('Ghost adapter', () => { assert.equal(packet.tokenSource.kind, 'summon-default'); assert.equal('css' in packet.tokenSource, false); }); - - it('resolves caller-provided Ghost context without repo access', async () => { - const roots = parseGhostRoots(''); - const parsed = parseGhostRequest({ - source: 'resolved-context', - id: 'checkout', - product: 'Checkout', - prompt: 'You are working inside the Checkout product experience.', - provenance: { layers: ['portable'] }, - }, roots); - assert.equal(parsed.ok, true); - if (!parsed.ok || !parsed.request) assert.fail('expected valid resolved context request'); - - const ctx = await resolveGhostSteer(parsed.request, roots); - - assert.equal(ctx.source, 'resolved-context'); - assert.equal(ctx.prompt, 'You are working inside the Checkout product experience.'); - assert.equal(ctx.product, 'Checkout'); - assert.equal(ctx.root, null); - assert.equal(ctx.stack, null); - assert.equal(ctx.tokenSource.kind, 'summon-default'); - assert.deepEqual(ctx.provenance, { layers: ['portable'] }); - }); - - it('uses valid resolved-context tokens', async () => { - const tokens = await readDefaultTokensCss(); - const roots = parseGhostRoots(''); - const parsed = parseGhostRequest({ - source: 'resolved-context', - prompt: 'Use portable Ghost memory.', - tokensCss: tokens, - tokenSource: 'bundle/tokens.css', - }, roots); - assert.equal(parsed.ok, true); - if (!parsed.ok || !parsed.request) assert.fail('expected valid resolved context request'); - - const ctx = await resolveGhostSteer(parsed.request, roots); - - assert.equal(ctx.source, 'resolved-context'); - assert.equal(ctx.tokenSource.kind, 'resolved-context'); - assert.equal(ctx.tokenSource.source, 'bundle/tokens.css'); - assert.equal(ctx.tokenSource.css, tokens); - }); - - it('falls back from invalid resolved-context tokens to base direction tokens', async () => { - const baseTokens = await readDefaultTokensCss(); - const roots = parseGhostRoots(''); - const parsed = parseGhostRequest({ - source: 'resolved-context', - prompt: 'Use portable Ghost memory.', - tokensCss: ':root { --color-bg: red; }', - tokenSource: 'bundle/tokens.css', - baseDirectionId: 'ghost', - }, roots); - assert.equal(parsed.ok, true); - if (!parsed.ok || !parsed.request) assert.fail('expected valid resolved context request'); - - const ctx = await resolveGhostSteer(parsed.request, roots, { - id: 'ghost', - tokensCss: baseTokens, - }); - - assert.equal(ctx.source, 'resolved-context'); - assert.equal(ctx.tokenSource.kind, 'base-direction'); - assert.equal(ctx.tokenSource.source, 'direction:ghost/tokens.css'); - assert.ok(ctx.tokenSource.warnings.some((warning) => warning.includes('bundle/tokens.css failed token contract'))); - }); - - it('rejects resolved-context requests without prompt', () => { - const roots = parseGhostRoots(''); - - assert.deepEqual(parseGhostRequest({ source: 'resolved-context' }, roots), { - ok: false, - error: 'ghost.prompt is required for resolved-context', - }); - assert.deepEqual(parseGhostRequest({ source: 'resolved-context', prompt: ' ' }, roots), { - ok: false, - error: 'ghost.prompt is required for resolved-context', - }); - }); }); async function makeGhostFixture(options: { tokenCss?: string; large?: boolean } = {}): Promise { @@ -356,7 +224,6 @@ async function makeGhostFixture(options: { tokenCss?: string; large?: boolean } await mkdir(join(root, '.ghost', 'fingerprint', 'memory'), { recursive: true }); const extraPrinciples = options.large ? Array.from({ length: 30 }, (_, index) => ` - id: extra-principle-${index} - status: accepted principle: Extra accepted principle ${index} ${'keeps operational hierarchy clear '.repeat(12)} guidance: - Extra guidance ${index} ${'keeps the surface compact and legible '.repeat(10)} @@ -364,8 +231,7 @@ async function makeGhostFixture(options: { tokenCss?: string; large?: boolean } : []; const extraPatterns = options.large ? Array.from({ length: 30 }, (_, index) => ` - id: extra-pattern-${index} - kind: composition - status: accepted + kind: structure pattern: Extra composition pattern ${index} ${'uses restrained blocks and clear state '.repeat(12)} guidance: - Extra pattern guidance ${index} ${'prevents ornamental layout from hiding work '.repeat(10)} @@ -379,8 +245,7 @@ id: test-product ); await writeFile( join(root, '.ghost', 'fingerprint', 'prose.yml'), - `schema: ghost.fingerprint-prose/v1 -summary: + `summary: product: Test Product audience: [operators] goals: [keep work legible] @@ -391,15 +256,13 @@ situations: user_intent: Show the current checkout queue state. product_obligation: Keep operator status legible before secondary detail. surface_type: dashboard - paths: [.] - principles: [principle:calm-density] - experience_contracts: [experience_contract:queue-trust] - patterns: [pattern:measured-surfaces] + principles: [prose.principle:calm-density] + experience_contracts: [prose.experience_contract:queue-trust] + patterns: [composition.pattern:measured-surfaces] refuses: - Decorative chrome that hides operational state. principles: - id: calm-density - status: accepted principle: Preserve quiet density and clear hierarchy. applies_to: paths: [.] @@ -411,7 +274,6 @@ principles: check_refs: [check:no-rainbow] ${extraPrinciples.join('')}experience_contracts: - id: queue-trust - status: accepted contract: Status surfaces must foreground current state. applies_to: paths: [.] @@ -423,8 +285,7 @@ ${extraPrinciples.join('')}experience_contracts: ); await writeFile( join(root, '.ghost', 'fingerprint', 'inventory.yml'), - `schema: ghost.fingerprint-inventory/v1 -topology: + `topology: scopes: - id: app paths: [.] @@ -438,11 +299,9 @@ building_blocks: ); await writeFile( join(root, '.ghost', 'fingerprint', 'composition.yml'), - `schema: ghost.fingerprint-composition/v1 -patterns: + `patterns: - id: measured-surfaces - kind: composition - status: accepted + kind: structure pattern: Surfaces are compact, rectangular, and information-first. applies_to: paths: [.] @@ -461,9 +320,19 @@ ${extraPatterns.join('')} id: test-product checks: - id: no-rainbow - active: true - summary: Avoid rainbow decorative color. - pattern: rainbow + title: Avoid rainbow decorative color + status: active + severity: serious + applies_to: + paths: [.] + detector: + type: forbidden-regex + pattern: rainbow + evidence: + support: 1 + observed_count: 1 + examples: + - queue fixture avoids rainbow decorative color `, ); await writeFile( @@ -490,46 +359,6 @@ libraries: [] return root; } -async function makeLegacyGhostFixture(): Promise { - const root = await mkdtemp(join(tmpdir(), 'summon-ghost-adapter-legacy-')); - fixtureRoots.push(root); - await mkdir(join(root, '.ghost'), { recursive: true }); - await writeFile( - join(root, '.ghost', 'fingerprint.yml'), - `schema: ghost.fingerprint/v1 -summary: - product: Test Product - audience: [operators] - goals: [keep work legible] - tone: [quiet, exacting workflows] -topology: - scopes: - - id: app - paths: [.] - surface_types: [dashboard] - surface_types: [dashboard] -situations: [] -principles: - - id: calm-density - status: accepted - principle: Preserve quiet density and clear hierarchy. -experience_contracts: [] -patterns: - - id: measured-surfaces - kind: visual - status: accepted - pattern: Surfaces are compact, rectangular, and information-first. -implementation_vocabulary: - tokens: [--color-bg, --color-text] - components: [] -review_policy: - proposal_policy: - - Agents propose memory changes; humans promote durable truth. -`, - ); - return root; -} - async function readDefaultTokensCss(): Promise { const here = dirname(fileURLToPath(import.meta.url)); return readFile( diff --git a/apps/server/src/ghost-adapter.ts b/apps/server/src/ghost-adapter.ts index 309e3eb..69b7177 100644 --- a/apps/server/src/ghost-adapter.ts +++ b/apps/server/src/ghost-adapter.ts @@ -6,6 +6,11 @@ import { type SurfacePlan, } from '@anarchitecture/summon/engine'; import type { GhostGenerationContext } from '@anarchitecture/summon-server'; +import { readOptionalPackageConfig } from '@anarchitecture/ghost/fingerprint'; +import { + gatherRelayContext, + type RelayGatherResult, +} from '@anarchitecture/ghost/relay'; import { existsSync, readFileSync, statSync } from 'node:fs'; import { dirname, @@ -14,22 +19,6 @@ import { resolve, } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { - normalizeGhostMemoryDir, - readGhostPackageConfig, - resolveGhostRootCompat, - type GhostStackCompat, - type GhostStackLayerCompat, - type GhostContextCompat, -} from './ghost-scan-compat.js'; -import { - buildSummonGhostCapsule, - ghostCapsuleMeta, - ghostContextMode, - type SummonGhostCapsule, -} from './ghost-capsule.js'; - -export { ghostCapsuleMeta }; const ROOT_ID_RE = /^[a-z][a-z0-9._-]{0,63}$/; @@ -47,6 +36,9 @@ const DEFAULT_TOKENS_CSS = readFileSync( 'utf-8', ); +type RelayStackSource = Extract; +type RelayStackLayer = RelayStackSource['provenance']['layers'][number]; + export interface GhostRootRequest { source: 'root'; rootId: string; @@ -55,18 +47,7 @@ export interface GhostRootRequest { baseDirectionId: string | null; } -export interface GhostResolvedContextRequest { - source: 'resolved-context'; - id?: string; - product?: string; - prompt: string; - tokensCss?: string; - tokenSource?: string; - provenance?: unknown; - baseDirectionId: string | null; -} - -export type GhostRequest = GhostRootRequest | GhostResolvedContextRequest; +export type GhostRequest = GhostRootRequest; export interface GhostRoot { id: string; @@ -76,7 +57,7 @@ export interface GhostRoot { export type GhostRoots = Map; export interface GhostTokenSource { - kind: 'ghost-config' | 'resolved-context' | 'base-direction' | 'summon-default'; + kind: 'ghost-config' | 'base-direction' | 'summon-default'; source: string; css: string; warnings: string[]; @@ -92,29 +73,14 @@ export interface ResolvedRootGhostSteer extends GhostGenerationContext { source: 'root'; request: GhostRootRequest; root: string; - stack: GhostStackCompat; - context: GhostContextCompat; + relay: RelayGatherResult & { source: RelayStackSource }; prompt: string; tokenSource: GhostTokenSource; baseDirectionId: string | null; product: string; - capsule?: SummonGhostCapsule; } -export interface ResolvedContextGhostSteer extends GhostGenerationContext { - source: 'resolved-context'; - request: GhostResolvedContextRequest; - root: null; - stack: null; - context: null; - prompt: string; - tokenSource: GhostTokenSource; - baseDirectionId: string | null; - product?: string; - provenance?: unknown; -} - -export type ResolvedGhostSteer = ResolvedRootGhostSteer | ResolvedContextGhostSteer; +export type ResolvedGhostSteer = ResolvedRootGhostSteer; export type ResolvedGhostContext = ResolvedGhostSteer; @@ -128,21 +94,24 @@ export interface GhostSurfacePromptOptions { } export interface GhostReviewPacket { - schema: 'summon.ghost-generation/v1'; - source: ResolvedGhostSteer['source']; + schema: 'summon.ghost-fingerprint-generation/v1'; + source: 'root'; prompt: string; - rootId: string | null; - targetPath: string | null; - memoryDir: string | null; + rootId: string; + targetPath: string; + memoryDir: string; product: string; layers: string[]; - memoryProvenance: { - merge: GhostStackCompat['provenance']['merge'] | 'external'; + fingerprintProvenance: { + merge: RelayStackSource['provenance']['merge']; layers: Array<{ relativeRoot: string; memoryDir: string; + dir: string; }>; - provenance?: unknown; + layerDirs: string[]; + targetPaths: string[]; + match: RelayGatherResult['entrypoint']['match']; }; tokenSource: Omit; baseDirectionId: string | null; @@ -205,45 +174,16 @@ export function parseGhostRequest( const source = obj.source === undefined || obj.source === null || obj.source === '' ? 'root' : obj.source; - if (source !== 'root' && source !== 'resolved-context') { - return { ok: false, error: 'ghost.source must be "root" or "resolved-context"' }; + if (source !== 'root') { + return { + ok: false, + error: 'ghost.source must be "root"; resolved-context is no longer supported', + }; } const baseDirectionId = parseBaseDirectionId(obj.baseDirectionId); if (!baseDirectionId.ok) return { ok: false, error: baseDirectionId.error }; - if (source === 'resolved-context') { - const prompt = typeof obj.prompt === 'string' ? obj.prompt.trim() : ''; - if (!prompt) { - return { ok: false, error: 'ghost.prompt is required for resolved-context' }; - } - const request: GhostResolvedContextRequest = { - source: 'resolved-context', - prompt, - baseDirectionId: baseDirectionId.value, - }; - if (obj.id !== undefined && (typeof obj.id !== 'string' || !obj.id.trim())) { - return { ok: false, error: 'ghost.id must be a non-empty string when provided' }; - } - if (typeof obj.id === 'string') request.id = obj.id.trim(); - if (obj.product !== undefined && (typeof obj.product !== 'string' || !obj.product.trim())) { - return { ok: false, error: 'ghost.product must be a non-empty string when provided' }; - } - if (typeof obj.product === 'string') request.product = obj.product.trim(); - if (obj.tokensCss !== undefined && typeof obj.tokensCss !== 'string') { - return { ok: false, error: 'ghost.tokensCss must be a string when provided' }; - } - if (typeof obj.tokensCss === 'string' && obj.tokensCss.trim()) request.tokensCss = obj.tokensCss; - if (obj.tokenSource !== undefined && (typeof obj.tokenSource !== 'string' || !obj.tokenSource.trim())) { - return { ok: false, error: 'ghost.tokenSource must be a non-empty string when provided' }; - } - if (typeof obj.tokenSource === 'string' && obj.tokenSource.trim()) { - request.tokenSource = obj.tokenSource.trim(); - } - if (obj.provenance !== undefined) request.provenance = obj.provenance; - return { ok: true, request }; - } - if (typeof obj.rootId !== 'string' || !ROOT_ID_RE.test(obj.rootId)) { return { ok: false, error: 'ghost.rootId must be a configured root id' }; } @@ -301,29 +241,6 @@ export async function resolveGhostGenerationContext( roots: GhostRoots, baseDirection: GhostBaseDirection | null = null, ): Promise { - if (request.source === 'resolved-context') { - const tokenSource = resolveResolvedContextTokenSource(request, baseDirection); - return { - source: 'resolved-context', - request, - root: null, - stack: null, - context: null, - prompt: request.prompt, - product: request.product, - tokenSource, - provenance: request.provenance, - baseDirectionId: baseDirection?.id ?? request.baseDirectionId ?? null, - }; - } - return resolveRootGhostGenerationContext(request, roots, baseDirection); -} - -async function resolveRootGhostGenerationContext( - request: GhostRootRequest, - roots: GhostRoots, - baseDirection: GhostBaseDirection | null, -): Promise { const root = roots.get(request.rootId); if (!root) throw new Error(`unknown Ghost root "${request.rootId}"`); const targetAbs = resolve(root, request.targetPath); @@ -331,31 +248,26 @@ async function resolveRootGhostGenerationContext( throw new Error('ghost.targetPath must stay within the configured root'); } - const resolved = await resolveGhostRootCompat({ - root, - targetPath: request.targetPath, + const relay = await gatherRelayContext({ + cwd: root, + target: request.targetPath, memoryDir: request.memoryDir, }); - const { stack, context } = resolved; - if (resolve(stack.repoRoot) !== resolve(root)) { + if (relay.source.kind !== 'stack') { + throw new Error('Ghost relay did not resolve a fingerprint stack source'); + } + if (resolve(relay.source.repoRoot) !== resolve(root)) { throw new Error('configured Ghost root must resolve to the fingerprint stack repo root'); } - const product = stack.product ?? context.name; - const [promptContext, tokenSource] = await Promise.all([ - buildPromptFromContext(context, { - product, - targetPath: stack.targetPath, - }), - resolveGhostTokenSource(stack, baseDirection), - ]); + const stackRelay = relay as RelayGatherResult & { source: RelayStackSource }; + const product = relay.entrypoint.identity.product || relay.name || request.rootId; + const tokenSource = await resolveGhostTokenSource(stackRelay, baseDirection); return { source: 'root', request, root, - stack, - context, - prompt: promptContext.prompt, - ...(promptContext.capsule ? { capsule: promptContext.capsule } : {}), + relay: stackRelay, + prompt: relay.brief, product, tokenSource, baseDirectionId: baseDirection?.id ?? request.baseDirectionId ?? null, @@ -366,55 +278,26 @@ export function prepareGhostSurfacePrompt( context: ResolvedGhostSteer, options: GhostSurfacePromptOptions, ): ResolvedGhostSteer { - if (context.source !== 'root' || ghostContextMode() === 'raw') return context; - const capsule = buildSummonGhostCapsule({ - raw: context.context.raw, - product: context.product, - targetPath: context.stack.targetPath, - userPrompt: options.userPrompt, - mode: options.mode, - surfacePlan: options.surfacePlan, - shape: options.shape, - capabilities: options.capabilities, - components: options.components, - }); return { ...context, - prompt: capsule.prompt, - capsule, + prompt: [ + context.prompt.trim(), + buildSummonFingerprintSurfaceBrief(context, options), + ].filter(Boolean).join('\n\n'), }; } export function ghostContextMeta(ctx: ResolvedGhostContext) { - if (ctx.source === 'resolved-context') { - return { - source: ctx.source, - rootId: ctx.request.id ?? null, - targetPath: null, - memoryDir: null, - layers: [], - product: ctx.product ?? ctx.request.id ?? 'Ghost', - baseDirectionId: ctx.baseDirectionId, - styleSource: ctx.tokenSource.kind, - provenance: ctx.provenance ?? null, - }; - } return { source: ctx.source, rootId: ctx.request.rootId, - targetPath: ctx.stack.targetPath, - memoryDir: ctx.stack.memoryDir, - layers: ctx.stack.layers.map((layer) => layer.relativeRoot), + targetPath: ctx.relay.source.targetPath, + memoryDir: ctx.relay.source.fingerprintDir, + layers: ctx.relay.source.provenance.layers.map((layer: RelayStackLayer) => layer.relative_root), product: ctx.product, baseDirectionId: ctx.baseDirectionId, styleSource: ctx.tokenSource.kind, - provenance: { - merge: ctx.stack.provenance.merge, - layers: ctx.stack.provenance.layers.map((layer) => ({ - relativeRoot: layer.relativeRoot, - memoryDir: layer.memoryDir, - })), - }, + provenance: fingerprintProvenance(ctx.relay), }; } @@ -442,38 +325,16 @@ export function buildGhostReviewPacket(input: { if (line.op !== 'add' || !line.path.startsWith('/section/')) continue; sectionsById.set(line.path.slice('/section/'.length), line.html ?? ''); } - const rootFields = input.context.source === 'root' - ? { - rootId: input.context.request.rootId, - targetPath: input.context.stack.targetPath, - memoryDir: input.context.stack.memoryDir, - product: input.context.product, - layers: input.context.stack.layers.map((layer) => layer.relativeRoot), - memoryProvenance: { - merge: input.context.stack.provenance.merge, - layers: input.context.stack.provenance.layers.map((layer) => ({ - relativeRoot: layer.relativeRoot, - memoryDir: layer.memoryDir, - })), - }, - } - : { - rootId: input.context.request.id ?? null, - targetPath: null, - memoryDir: null, - product: input.context.product ?? input.context.request.id ?? 'Ghost', - layers: [], - memoryProvenance: { - merge: 'external' as const, - layers: [], - provenance: input.context.provenance ?? null, - }, - }; return { - schema: 'summon.ghost-generation/v1', + schema: 'summon.ghost-fingerprint-generation/v1', source: input.context.source, prompt: input.prompt, - ...rootFields, + rootId: input.context.request.rootId, + targetPath: input.context.relay.source.targetPath, + memoryDir: input.context.relay.source.fingerprintDir, + product: input.context.product, + layers: input.context.relay.source.provenance.layers.map((layer: RelayStackLayer) => layer.relative_root), + fingerprintProvenance: fingerprintProvenance(input.context.relay), tokenSource: { kind: input.context.tokenSource.kind, source: input.context.tokenSource.source, @@ -490,37 +351,52 @@ export function buildGhostReviewPacket(input: { }; } -async function buildPromptFromContext( - context: GhostContextCompat, - input: { - product: string; - targetPath: string; - }, -): Promise<{ prompt: string; capsule?: SummonGhostCapsule }> { - if (ghostContextMode() === 'raw') { - return { prompt: await context.writePrompt() }; - } - const capsule = buildSummonGhostCapsule({ - raw: context.raw, - product: input.product, - targetPath: input.targetPath, - }); - return { prompt: capsule.prompt, capsule }; +function buildSummonFingerprintSurfaceBrief( + context: ResolvedGhostSteer, + options: GhostSurfacePromptOptions, +): string { + const capabilityNames = options.capabilities?.intents.map((intent) => intent.name) ?? []; + const componentNames = options.components?.components.map((component) => component.name) ?? []; + const details = [ + `Product: ${context.product}`, + `Target path: ${context.relay.source.targetPath}`, + `User request: ${oneLine(options.userPrompt, 600)}`, + `Surface plan: purpose=${options.surfacePlan.purpose}; runtime=${options.surfacePlan.runtime}; data=${options.surfacePlan.data}; authority=${options.surfacePlan.authority}; persistence=${options.surfacePlan.persistence}`, + `Mode: ${options.mode}`, + options.shape ? `Response shape hint: ${options.shape}` : null, + capabilityNames.length > 0 ? `Granted host capabilities: ${capabilityNames.join(', ')}` : 'Granted host capabilities: none', + componentNames.length > 0 ? `Granted host components: ${componentNames.join(', ')}` : null, + ].filter((line): line is string => Boolean(line)); + + return [ + '## Summon Surface Brief', + '', + 'Treat the Ghost fingerprint above as a product design direction package for this Summon surface.', + '', + ...details.map((line) => `- ${line}`), + '', + 'Generation rules:', + '', + '- Compose from the fingerprint prose, inventory, and composition layers. Prose states intent; inventory supplies material and evidence; composition supplies reusable surface patterns.', + '- Do not imitate Ghost UI as a visual style. Use inventory examples only when they support the selected intent and composition pattern.', + '- The agent broker controls host authority and capabilities. The fingerprint controls product direction, hierarchy, tone, and composition expectations.', + '- Treat checks as validation constraints, not as content to render.', + ].join('\n'); } async function resolveGhostTokenSource( - stack: GhostStackCompat, + relay: RelayGatherResult & { source: RelayStackSource }, baseDirection: GhostBaseDirection | null, ): Promise { const warnings: string[] = []; - for (const layer of [...stack.layers].reverse()) { - const configPath = resolve(layer.root, layer.memoryDir, 'config.yml'); + for (const layer of [...relay.source.provenance.layers].reverse()) { + const configPath = resolve(layer.dir, 'config.yml'); let config; try { - config = await readGhostPackageConfig(configPath); + config = await readOptionalPackageConfig(configPath); } catch (err) { warnings.push( - `${displayPath(stack, configPath)} could not be read: ${ + `${displayPath(relay.source, configPath)} could not be read: ${ err instanceof Error ? err.message : String(err) }`, ); @@ -537,7 +413,7 @@ async function resolveGhostTokenSource( continue; } if (!existsSync(tokenPath) || !statSync(tokenPath).isFile()) { - warnings.push(`${displayPath(stack, tokenPath)} not found`); + warnings.push(`${displayPath(relay.source, tokenPath)} not found`); continue; } const css = readFileSync(tokenPath, 'utf-8'); @@ -545,13 +421,13 @@ async function resolveGhostTokenSource( const blocking = validation.issues.filter((issue) => issue.severity === 'block'); if (blocking.length > 0) { warnings.push( - `${displayPath(stack, tokenPath)} failed token contract: ${blocking.map((issue) => issue.message).join('; ')}`, + `${displayPath(relay.source, tokenPath)} failed token contract: ${blocking.map((issue) => issue.message).join('; ')}`, ); continue; } return { kind: 'ghost-config', - source: displayPath(stack, tokenPath), + source: displayPath(relay.source, tokenPath), css, warnings: [ ...warnings, @@ -566,33 +442,6 @@ async function resolveGhostTokenSource( return resolveFallbackTokenSource(warnings, baseDirection); } -function resolveResolvedContextTokenSource( - request: GhostResolvedContextRequest, - baseDirection: GhostBaseDirection | null, -): GhostTokenSource { - const warnings: string[] = []; - if (request.tokensCss) { - const validation = compileTokenContract({ css: request.tokensCss }); - const blocking = validation.issues.filter((issue) => issue.severity === 'block'); - if (blocking.length === 0) { - return { - kind: 'resolved-context', - source: request.tokenSource ?? 'resolved-context:tokensCss', - css: request.tokensCss, - warnings: [ - ...validation.issues - .filter((issue) => issue.severity === 'warn') - .map((issue) => issue.message), - ], - }; - } - warnings.push( - `${request.tokenSource ?? 'resolved-context:tokensCss'} failed token contract: ${blocking.map((issue) => issue.message).join('; ')}`, - ); - } - return resolveFallbackTokenSource(warnings, baseDirection); -} - function resolveFallbackTokenSource( warnings: string[], baseDirection: GhostBaseDirection | null, @@ -608,7 +457,7 @@ function resolveFallbackTokenSource( baseDirectionId: baseDirection.id, warnings: [ ...warnings, - 'No contract-complete Ghost token CSS was found; using the base Summon direction tokens.', + 'No contract-complete Ghost fingerprint token CSS was found; using the fallback Summon direction tokens.', ...validation.issues .filter((issue) => issue.severity === 'warn') .map((issue) => issue.message), @@ -625,13 +474,13 @@ function resolveFallbackTokenSource( css: DEFAULT_TOKENS_CSS, warnings: [ ...warnings, - 'No contract-complete Ghost token CSS was found; using Summon default tokens.', + 'No contract-complete Ghost fingerprint token CSS was found; using Summon default tokens.', ], }; } function resolveTokenPath( - layer: GhostStackLayerCompat, + layer: RelayStackLayer, rawRef: string, ): string | null { const raw = rawRef.trim(); @@ -660,6 +509,33 @@ function resolveTokenPath( return isWithinOrEqual(layer.root, resolved) ? resolved : null; } +function fingerprintProvenance(relay: RelayGatherResult & { source: RelayStackSource }): GhostReviewPacket['fingerprintProvenance'] { + return { + merge: relay.source.provenance.merge, + layers: relay.source.provenance.layers.map((layer: RelayStackLayer) => ({ + relativeRoot: layer.relative_root, + memoryDir: layer.fingerprint_dir, + dir: displayPath(relay.source, layer.dir), + })), + layerDirs: relay.layerDirs.map((dir: string) => displayPath(relay.source, dir)), + targetPaths: relay.targetPaths, + match: relay.entrypoint.match, + }; +} + +function normalizeGhostMemoryDir(raw: string): string { + const normalized = raw.trim().replaceAll('\\', '/').replace(/\/+/g, '/').replace(/\/$/g, ''); + if (!normalized || normalized === '.') return '.ghost'; + if (normalized.startsWith('/') || isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized)) { + throw new Error('ghost.memoryDir must be relative'); + } + const segments = normalized.split('/'); + if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) { + throw new Error('ghost.memoryDir must not contain path traversal segments'); + } + return segments.join('/'); +} + function parseBaseDirectionId(raw: unknown): | { ok: true; value: string | null } | { ok: false; error: string } { @@ -711,12 +587,17 @@ function declaredSectionsFromLines(lines: ProtocolLine[]): string[] { return []; } +function oneLine(value: string, max: number): string { + const compact = value.replace(/\s+/g, ' ').trim(); + return compact.length <= max ? compact : `${compact.slice(0, Math.max(0, max - 3))}...`; +} + function isWithinOrEqual(root: string, child: string): boolean { const rel = relative(resolve(root), resolve(child)); return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); } -function displayPath(stack: GhostStackCompat, absPath: string): string { - const rel = relative(stack.repoRoot, absPath); +function displayPath(source: RelayStackSource, absPath: string): string { + const rel = relative(source.repoRoot, absPath); return rel && !rel.startsWith('..') && !isAbsolute(rel) ? rel : absPath; } diff --git a/apps/server/src/ghost-capsule.ts b/apps/server/src/ghost-capsule.ts deleted file mode 100644 index b857531..0000000 --- a/apps/server/src/ghost-capsule.ts +++ /dev/null @@ -1,699 +0,0 @@ -import type { - CapabilityPack, - ComponentPack, - SurfacePlan, -} from '@anarchitecture/summon/engine'; - -export type GhostCapsuleMode = 'capsule' | 'raw'; - -export interface SummonGhostCapsule { - schema: 'summon.ghost-capsule/v1'; - mode: 'capsule'; - product: string; - targetPath: string; - surface: { - mode: 'static' | 'interactive'; - purpose: string | null; - runtime: string | null; - data: string | null; - authority: string | null; - persistence: string | null; - shape: string | null; - }; - selectedRefs: { - situations: string[]; - principles: string[]; - experienceContracts: string[]; - patterns: string[]; - checks: string[]; - }; - obligations: string[]; - composition: string[]; - visualRules: string[]; - contentRules: string[]; - avoid: string[]; - vocabulary: { - tokens: string[]; - components: string[]; - libraries: string[]; - }; - intentNote?: string; - warnings: string[]; - budgetChars: number; - prompt: string; -} - -export interface GhostCapsuleBuildInput { - raw: unknown; - product: string; - targetPath: string; - userPrompt?: string; - mode?: 'static' | 'interactive'; - surfacePlan?: SurfacePlan | null; - shape?: string | null; - capabilities?: CapabilityPack | null; - components?: ComponentPack | null; -} - -interface NormalizedGhostMemory { - summary: Record; - topology: Record; - situations: GhostSituation[]; - principles: GhostPrinciple[]; - experienceContracts: GhostExperienceContract[]; - patterns: GhostPattern[]; - vocabulary: { - tokens: string[]; - components: string[]; - libraries: string[]; - }; - checks: GhostCheck[]; - intent: string | null; - warnings: string[]; -} - -interface GhostScoped { - applies_to?: Record; - paths?: unknown; - surface_types?: unknown; - situations?: unknown; -} - -interface GhostSituation extends GhostScoped { - id: string; - title?: string; - user_intent?: string; - product_obligation?: string; - surface_type?: string; - hierarchy?: Record; - refuses?: string[]; - principles?: string[]; - experience_contracts?: string[]; - patterns?: string[]; - evidence?: unknown; -} - -interface GhostPrinciple extends GhostScoped { - id: string; - status?: string; - principle?: string; - guidance?: string[]; - counterexamples?: string[]; - check_refs?: string[]; -} - -interface GhostExperienceContract extends GhostScoped { - id: string; - status?: string; - contract?: string; - obligations?: string[]; - check_refs?: string[]; -} - -interface GhostPattern extends GhostScoped { - id: string; - status?: string; - kind?: string; - pattern?: string; - guidance?: string[]; - anti_patterns?: string[]; - check_refs?: string[]; -} - -interface GhostCheck { - id: string; - active: boolean; - summary?: string; - pattern?: string; - description?: string; -} - -const STATIC_BUDGET = 4000; -const INTERACTIVE_BUDGET = 6000; -const MAX_BULLET_CHARS = 190; - -export function ghostContextMode(env: NodeJS.ProcessEnv = process.env): GhostCapsuleMode { - return env.SUMMON_GHOST_CONTEXT_MODE?.trim().toLowerCase() === 'raw' ? 'raw' : 'capsule'; -} - -export function buildSummonGhostCapsule(input: GhostCapsuleBuildInput): SummonGhostCapsule { - const memory = normalizeGhostMemory(input.raw); - const surface = surfaceContext(input); - const queryTerms = queryTermsFor(input, surface); - const selectedSituations = rankItems(memory.situations, queryTerms, input, (item) => - [ - item.id, - item.title, - item.user_intent, - item.product_obligation, - item.surface_type, - ...(item.refuses ?? []), - ].filter(isString).join(' '), - ).slice(0, 2); - const situationRefs = refsFromSituations(selectedSituations); - - const selectedPrinciples = rankItems( - accepted(memory.principles), - queryTerms, - input, - (item) => [item.id, item.principle, ...(item.guidance ?? [])].filter(isString).join(' '), - (item) => situationRefs.principles.has(item.id) ? 80 : 0, - ).slice(0, 4); - const selectedContracts = rankItems( - accepted(memory.experienceContracts), - queryTerms, - input, - (item) => [item.id, item.contract, ...(item.obligations ?? [])].filter(isString).join(' '), - (item) => situationRefs.experienceContracts.has(item.id) ? 80 : 0, - ).slice(0, 4); - const selectedPatterns = rankItems( - accepted(memory.patterns), - queryTerms, - input, - (item) => [item.id, item.kind, item.pattern, ...(item.guidance ?? []), ...(item.anti_patterns ?? [])].filter(isString).join(' '), - (item) => (situationRefs.patterns.has(item.id) ? 80 : 0) + patternKindPriority(item.kind), - ).slice(0, 4); - - const selectedCheckIds = uniqueStrings([ - ...selectedPrinciples.flatMap((item) => item.check_refs ?? []), - ...selectedContracts.flatMap((item) => item.check_refs ?? []), - ...selectedPatterns.flatMap((item) => item.check_refs ?? []), - ].map(stripRefPrefix)); - const activeChecks = memory.checks - .filter((check) => check.active) - .filter((check) => selectedCheckIds.length === 0 || selectedCheckIds.includes(check.id)) - .slice(0, 5); - - const selectedRefs = { - situations: selectedSituations.map((item) => item.id), - principles: selectedPrinciples.map((item) => item.id), - experienceContracts: selectedContracts.map((item) => item.id), - patterns: selectedPatterns.map((item) => item.id), - checks: activeChecks.map((item) => item.id), - }; - - const obligations = uniqueStrings([ - ...selectedSituations.map((item) => item.product_obligation), - ...selectedContracts.map((item) => item.contract), - ...selectedContracts.flatMap((item) => item.obligations ?? []), - ...selectedPrinciples.map((item) => item.principle), - ].filter(isString)).slice(0, 5); - const composition = uniqueStrings([ - ...selectedPatterns - .filter((item) => item.kind === 'composition' || item.kind === 'visual' || !item.kind) - .flatMap((item) => [item.pattern, ...(item.guidance ?? [])]), - ].filter(isString)).slice(0, 4); - const visualRules = uniqueStrings([ - ...stringArray(memory.summary.tone).map((tone) => `Tone: ${tone}`), - ...selectedPrinciples.flatMap((item) => item.guidance ?? []), - ...selectedPatterns - .filter((item) => item.kind === 'visual') - .flatMap((item) => [item.pattern, ...(item.guidance ?? [])]), - ].filter(isString)).slice(0, 5); - const contentRules = uniqueStrings([ - ...selectedPatterns - .filter((item) => item.kind === 'content' || item.kind === 'behavioral') - .flatMap((item) => [item.pattern, ...(item.guidance ?? [])]), - ...selectedSituations.flatMap((item) => Object.values(item.hierarchy ?? {}).filter(isString)), - ].filter(isString)).slice(0, 4); - const avoid = uniqueStrings([ - ...selectedSituations.flatMap((item) => item.refuses ?? []), - ...selectedPatterns.flatMap((item) => item.anti_patterns ?? []), - ...selectedPrinciples.flatMap((item) => item.counterexamples ?? []), - ...activeChecks.map((check) => check.summary ?? check.description ?? check.pattern), - ].filter(isString)).slice(0, 5); - const vocabulary = filterVocabulary(memory.vocabulary, input.components); - - const baseCapsule = { - schema: 'summon.ghost-capsule/v1' as const, - mode: 'capsule' as const, - product: input.product, - targetPath: input.targetPath || '.', - surface, - selectedRefs, - obligations, - composition, - visualRules, - contentRules, - avoid, - vocabulary, - ...(memory.intent ? { intentNote: truncateSentence(memory.intent, 240) } : {}), - warnings: memory.warnings, - budgetChars: surface.mode === 'interactive' || surface.runtime === 'declarative' || surface.runtime === 'worker' - ? INTERACTIVE_BUDGET - : STATIC_BUDGET, - }; - - const prompt = renderCapsuleWithinBudget(baseCapsule); - return { - ...baseCapsule, - prompt, - }; -} - -export function ghostCapsuleMeta(capsule: SummonGhostCapsule) { - return { - schema: capsule.schema, - mode: capsule.mode, - product: capsule.product, - targetPath: capsule.targetPath, - surface: capsule.surface, - selectedRefs: capsule.selectedRefs, - promptChars: capsule.prompt.length, - budgetChars: capsule.budgetChars, - warnings: capsule.warnings, - }; -} - -function normalizeGhostMemory(raw: unknown): NormalizedGhostMemory { - const warnings: string[] = []; - const obj = asRecord(raw); - if (!obj) { - return emptyMemory(['Ghost context was not structured; using a minimal capsule.']); - } - - const split = splitPackageFromRaw(obj); - if (split) return split; - - const fingerprint = asRecord(obj.fingerprint ?? asRecord(obj.merged)?.fingerprint); - if (fingerprint) { - const nestedSplit = splitPackageFromRaw({ - ...fingerprint, - checks: obj.checks ?? fingerprint.checks, - checksRaw: obj.checksRaw ?? fingerprint.checksRaw, - intent: obj.intent ?? fingerprint.intent, - }); - if (nestedSplit) return nestedSplit; - - return { - summary: asRecord(fingerprint.summary) ?? {}, - topology: asRecord(fingerprint.topology) ?? {}, - situations: records(fingerprint.situations).map(normalizeSituation), - principles: records(fingerprint.principles).map(normalizePrinciple), - experienceContracts: records(fingerprint.experience_contracts).map(normalizeExperienceContract), - patterns: records(fingerprint.patterns).map(normalizePattern), - vocabulary: normalizeVocabulary(fingerprint.implementation_vocabulary), - checks: checksFromRaw(obj.checks, obj.checksRaw), - intent: stringValue(obj.intent), - warnings, - }; - } - - return emptyMemory(['Ghost context shape was not recognized; using a minimal capsule.']); -} - -function splitPackageFromRaw(obj: Record): NormalizedGhostMemory | null { - const prose = asRecord(obj.prose); - const inventory = asRecord(obj.inventory); - const composition = asRecord(obj.composition); - if (!prose && !inventory && !composition) return null; - return { - summary: asRecord(prose?.summary) ?? {}, - topology: asRecord(inventory?.topology) ?? {}, - situations: records(prose?.situations).map(normalizeSituation), - principles: records(prose?.principles).map(normalizePrinciple), - experienceContracts: records(prose?.experience_contracts).map(normalizeExperienceContract), - patterns: records(composition?.patterns).map(normalizePattern), - vocabulary: normalizeVocabulary( - inventory?.building_blocks ?? inventory?.implementation_vocabulary, - ), - checks: checksFromRaw(obj.checks, obj.checksRaw), - intent: stringValue(obj.intent), - warnings: [], - }; -} - -function emptyMemory(warnings: string[]): NormalizedGhostMemory { - return { - summary: {}, - topology: {}, - situations: [], - principles: [], - experienceContracts: [], - patterns: [], - vocabulary: { tokens: [], components: [], libraries: [] }, - checks: [], - intent: null, - warnings, - }; -} - -function normalizeSituation(value: Record): GhostSituation { - const surfaceType = stringValue(value.surface_type); - return { - id: idValue(value.id), - title: stringValue(value.title) ?? undefined, - user_intent: stringValue(value.user_intent) ?? undefined, - product_obligation: stringValue(value.product_obligation) ?? undefined, - surface_type: surfaceType ?? undefined, - applies_to: asRecord(value.applies_to) ?? undefined, - paths: value.paths, - surface_types: value.surface_types ?? (surfaceType ? [surfaceType] : undefined), - hierarchy: asRecord(value.hierarchy) ?? undefined, - refuses: stringArray(value.refuses), - principles: stringArray(value.principles).map(stripRefPrefix), - experience_contracts: stringArray(value.experience_contracts).map(stripRefPrefix), - patterns: stringArray(value.patterns).map(stripRefPrefix), - evidence: value.evidence, - }; -} - -function normalizePrinciple(value: Record): GhostPrinciple { - return { - id: idValue(value.id), - status: stringValue(value.status) ?? undefined, - principle: stringValue(value.principle) ?? undefined, - applies_to: asRecord(value.applies_to) ?? undefined, - paths: value.paths, - surface_types: value.surface_types, - situations: value.situations, - guidance: stringArray(value.guidance), - counterexamples: stringArray(value.counterexamples), - check_refs: stringArray(value.check_refs), - }; -} - -function normalizeExperienceContract(value: Record): GhostExperienceContract { - return { - id: idValue(value.id), - status: stringValue(value.status) ?? undefined, - contract: stringValue(value.contract) ?? undefined, - applies_to: asRecord(value.applies_to) ?? undefined, - paths: value.paths, - surface_types: value.surface_types, - situations: value.situations, - obligations: stringArray(value.obligations), - check_refs: stringArray(value.check_refs), - }; -} - -function normalizePattern(value: Record): GhostPattern { - return { - id: idValue(value.id), - status: stringValue(value.status) ?? undefined, - kind: stringValue(value.kind) ?? undefined, - pattern: stringValue(value.pattern) ?? undefined, - applies_to: asRecord(value.applies_to) ?? undefined, - paths: value.paths, - surface_types: value.surface_types, - situations: value.situations, - guidance: stringArray(value.guidance), - anti_patterns: stringArray(value.anti_patterns), - check_refs: stringArray(value.check_refs), - }; -} - -function normalizeVocabulary(raw: unknown): NormalizedGhostMemory['vocabulary'] { - const obj = asRecord(raw) ?? {}; - return { - tokens: stringArray(obj.tokens).slice(0, 20), - components: stringArray(obj.components).slice(0, 12), - libraries: stringArray(obj.libraries).slice(0, 8), - }; -} - -function checksFromRaw(rawChecks: unknown, rawYaml: unknown): GhostCheck[] { - const checksObj = asRecord(rawChecks); - const source = checksObj ?? parseChecksYaml(stringValue(rawYaml)); - return records(source?.checks).map((check) => ({ - id: idValue(check.id), - active: check.active !== false && stringValue(check.status) !== 'disabled', - summary: stringValue(check.summary) ?? undefined, - pattern: stringValue(check.pattern) ?? undefined, - description: stringValue(check.description) ?? undefined, - })); -} - -function parseChecksYaml(raw: string | null): Record | null { - if (!raw) return null; - const checksMatch = raw.match(/(?:^|\n)checks:\s*\n([\s\S]*)/); - if (!checksMatch) return null; - const checks = [...checksMatch[1]!.matchAll(/^\s*-\s+id:\s*([^\n]+)([\s\S]*?)(?=^\s*-\s+id:|\s*$)/gm)] - .map((match) => { - const body = match[2] ?? ''; - return { - id: cleanScalar(match[1] ?? ''), - active: !/^\s*active:\s*false\s*$/m.test(body), - summary: cleanScalar(body.match(/^\s*summary:\s*([^\n]+)/m)?.[1] ?? ''), - pattern: cleanScalar(body.match(/^\s*pattern:\s*([^\n]+)/m)?.[1] ?? ''), - }; - }); - return { checks }; -} - -function renderCapsuleWithinBudget(capsule: Omit): string { - let next = { ...capsule }; - let prompt = renderCapsule(next); - if (prompt.length <= capsule.budgetChars) return prompt; - - next = { - ...next, - obligations: next.obligations.slice(0, 4), - composition: next.composition.slice(0, 3), - visualRules: next.visualRules.slice(0, 3), - contentRules: next.contentRules.slice(0, 2), - avoid: next.avoid.slice(0, 3), - vocabulary: { - tokens: next.vocabulary.tokens.slice(0, 8), - components: next.vocabulary.components.slice(0, 6), - libraries: next.vocabulary.libraries.slice(0, 4), - }, - ...(next.intentNote ? { intentNote: truncateSentence(next.intentNote, 140) } : {}), - }; - prompt = renderCapsule(next); - if (prompt.length <= capsule.budgetChars) return prompt; - - return renderCapsule({ - ...next, - obligations: next.obligations.slice(0, 3), - composition: next.composition.slice(0, 2), - visualRules: [], - contentRules: [], - avoid: next.avoid.slice(0, 2), - vocabulary: { tokens: [], components: [], libraries: [] }, - intentNote: undefined, - warnings: [...next.warnings, 'Capsule was reduced to fit the prompt budget.'], - }).slice(0, capsule.budgetChars); -} - -function renderCapsule(capsule: Omit): string { - const lines = [ - `# ${capsule.product} Ghost Capsule`, - '', - 'Use this compact Ghost capsule as product-experience guidance for this Summon surface. It is a selected projection of durable Ghost memory, not the full fingerprint.', - '', - `- Target: \`${capsule.targetPath}\``, - `- Surface: ${[ - capsule.surface.purpose, - capsule.surface.runtime, - capsule.surface.data, - capsule.surface.authority, - capsule.surface.persistence, - ].filter(Boolean).join('/') || capsule.surface.mode}${capsule.surface.shape ? `; shape=${capsule.surface.shape}` : ''}`, - '', - refsLine(capsule), - section('Product Obligations', capsule.obligations), - section('Composition Guidance', capsule.composition), - section('Visual Guidance', capsule.visualRules), - section('Content Guidance', capsule.contentRules), - section('Avoid', capsule.avoid), - vocabularySection(capsule.vocabulary), - capsule.intentNote ? `## Human-Approved Intent\n\n${truncateSentence(capsule.intentNote, MAX_BULLET_CHARS)}` : '', - capsule.warnings.length ? section('Capsule Warnings', capsule.warnings.slice(0, 3)) : '', - ].filter(Boolean); - return `${lines.join('\n')}\n`; -} - -function refsLine(capsule: Omit): string { - const refs = [ - ...capsule.selectedRefs.situations.map((id) => `situation:${id}`), - ...capsule.selectedRefs.principles.map((id) => `principle:${id}`), - ...capsule.selectedRefs.experienceContracts.map((id) => `experience_contract:${id}`), - ...capsule.selectedRefs.patterns.map((id) => `pattern:${id}`), - ...capsule.selectedRefs.checks.map((id) => `check:${id}`), - ]; - return refs.length ? `Selected Ghost refs: ${refs.map((ref) => `\`${ref}\``).join(', ')}` : ''; -} - -function section(title: string, values: string[]): string { - if (values.length === 0) return ''; - return `## ${title}\n\n${values.map((value) => `- ${truncateSentence(value, MAX_BULLET_CHARS)}`).join('\n')}`; -} - -function vocabularySection(vocabulary: SummonGhostCapsule['vocabulary']): string { - const rows = [ - vocabulary.tokens.length ? `- Tokens: ${vocabulary.tokens.map((value) => `\`${value}\``).join(', ')}` : '', - vocabulary.components.length ? `- Components: ${vocabulary.components.map((value) => `\`${value}\``).join(', ')}` : '', - vocabulary.libraries.length ? `- Libraries: ${vocabulary.libraries.map((value) => `\`${value}\``).join(', ')}` : '', - ].filter(Boolean); - return rows.length ? `## Vocabulary\n\n${rows.join('\n')}` : ''; -} - -function surfaceContext(input: GhostCapsuleBuildInput): SummonGhostCapsule['surface'] { - return { - mode: input.mode ?? (input.surfacePlan?.runtime === 'static' ? 'static' : 'interactive'), - purpose: input.surfacePlan?.purpose ?? null, - runtime: input.surfacePlan?.runtime ?? null, - data: input.surfacePlan?.data ?? null, - authority: input.surfacePlan?.authority ?? null, - persistence: input.surfacePlan?.persistence ?? null, - shape: input.shape ?? null, - }; -} - -function queryTermsFor( - input: GhostCapsuleBuildInput, - surface: SummonGhostCapsule['surface'], -): Set { - return new Set([ - ...terms(input.userPrompt ?? ''), - ...terms(input.targetPath), - ...terms(surface.purpose ?? ''), - ...terms(surface.runtime ?? ''), - ...terms(surface.data ?? ''), - ...terms(surface.authority ?? ''), - ...terms(surface.shape ?? ''), - ]); -} - -function rankItems( - items: T[], - queryTerms: Set, - input: GhostCapsuleBuildInput, - textFor: (item: T) => string, - bonus: (item: T) => number = () => 0, -): T[] { - return [...items].sort((a, b) => - scoreItem(b, queryTerms, input, textFor(b)) + bonus(b) - - (scoreItem(a, queryTerms, input, textFor(a)) + bonus(a)), - ); -} - -function scoreItem( - item: GhostScoped, - queryTerms: Set, - input: GhostCapsuleBuildInput, - text: string, -): number { - let score = 0; - const scope = asRecord(item.applies_to) ?? item; - const paths = stringArray(scope.paths); - if (paths.some((path) => pathMatches(input.targetPath, path))) score += 60; - const surfaceTypes = stringArray(scope.surface_types); - if (surfaceTypes.some((type) => queryTerms.has(type.toLowerCase()))) score += 30; - const textTerms = terms(text); - for (const term of textTerms) { - if (queryTerms.has(term)) score += 4; - } - return score; -} - -function refsFromSituations(situations: GhostSituation[]) { - return { - principles: new Set(situations.flatMap((item) => item.principles ?? [])), - experienceContracts: new Set(situations.flatMap((item) => item.experience_contracts ?? [])), - patterns: new Set(situations.flatMap((item) => item.patterns ?? [])), - }; -} - -function filterVocabulary( - vocabulary: NormalizedGhostMemory['vocabulary'], - componentPack: ComponentPack | null | undefined, -): SummonGhostCapsule['vocabulary'] { - const allowedComponents = new Set( - componentPack?.components.map((component) => component.name) ?? [], - ); - const components = allowedComponents.size - ? vocabulary.components.filter((component) => allowedComponents.has(component)) - : vocabulary.components; - return { - tokens: vocabulary.tokens.slice(0, 12), - components: components.slice(0, 8), - libraries: vocabulary.libraries.slice(0, 6), - }; -} - -function accepted(items: T[]): T[] { - return items.filter((item) => !item.status || item.status === 'accepted'); -} - -function patternKindPriority(kind: string | undefined): number { - if (kind === 'composition') return 30; - if (kind === 'visual') return 20; - if (kind === 'content') return 10; - return 0; -} - -function pathMatches(targetPath: string, rawPath: string): boolean { - const target = normalizePath(targetPath || '.'); - const path = normalizePath(rawPath); - return path === '.' || target === path || target.startsWith(`${path}/`); -} - -function normalizePath(path: string): string { - return path.trim().replaceAll('\\', '/').replace(/\/+/g, '/').replace(/^\.\/?/, '') || '.'; -} - -function terms(text: string): string[] { - return text - .toLowerCase() - .split(/[^a-z0-9_-]+/) - .filter((term) => term.length >= 3); -} - -function records(value: unknown): Array> { - return Array.isArray(value) ? value.filter(isRecord) : []; -} - -function asRecord(value: unknown): Record | null { - return isRecord(value) ? value : null; -} - -function isRecord(value: unknown): value is Record { - return Boolean(value && typeof value === 'object' && !Array.isArray(value)); -} - -function stringArray(value: unknown): string[] { - return Array.isArray(value) ? value.filter(isString).map((item) => item.trim()).filter(Boolean) : []; -} - -function stringValue(value: unknown): string | null { - return typeof value === 'string' && value.trim() ? value.trim() : null; -} - -function isString(value: unknown): value is string { - return typeof value === 'string' && value.trim().length > 0; -} - -function idValue(value: unknown): string { - return stringValue(value) ?? 'unnamed'; -} - -function uniqueStrings(values: string[]): string[] { - const seen = new Set(); - const out: string[] = []; - for (const value of values) { - const normalized = value.trim(); - const key = normalized.toLowerCase(); - if (!normalized || seen.has(key)) continue; - seen.add(key); - out.push(normalized); - } - return out; -} - -function stripRefPrefix(ref: string): string { - const trimmed = ref.trim(); - const colon = trimmed.indexOf(':'); - return colon === -1 ? trimmed : trimmed.slice(colon + 1); -} - -function truncateSentence(value: string, max: number): string { - const cleaned = value.replace(/\s+/g, ' ').trim(); - if (cleaned.length <= max) return cleaned; - return `${cleaned.slice(0, Math.max(0, max - 3)).trimEnd()}...`; -} - -function cleanScalar(value: string): string { - return value.trim().replace(/^['"]|['"]$/g, ''); -} diff --git a/apps/server/src/ghost-scan-compat.ts b/apps/server/src/ghost-scan-compat.ts deleted file mode 100644 index b50886a..0000000 --- a/apps/server/src/ghost-scan-compat.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { existsSync, statSync } from 'node:fs'; -import { mkdir, readFile, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { - dirname, - isAbsolute, - join, - relative, - resolve, -} from 'node:path'; -import { parse as parseYaml } from 'yaml'; - -type GhostScanModule = Record; - -export interface GhostStackLayerCompat { - root: string; - relativeRoot: string; - memoryDir: string; -} - -export interface GhostStackCompat { - repoRoot: string; - targetPath: string; - memoryDir: string; - layers: GhostStackLayerCompat[]; - provenance: { - merge: string; - layers: Array<{ relativeRoot: string; memoryDir: string }>; - }; - product: string | null; - raw: unknown; -} - -export interface GhostContextCompat { - name: string; - raw: unknown; - writePrompt: () => Promise; -} - -export interface ResolvedGhostRootCompat { - stack: GhostStackCompat; - context: GhostContextCompat; -} - -let scanModulePromise: Promise | null = null; - -function loadScanModule(): Promise { - scanModulePromise ??= import('@anarchitecture/ghost/scan') as Promise; - return scanModulePromise; -} - -export function normalizeGhostMemoryDir(raw: string): string { - const normalized = raw.trim().replaceAll('\\', '/').replace(/\/+/g, '/'); - if (!normalized || normalized === '.') return '.ghost'; - if (normalized.startsWith('/') || isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized)) { - throw new Error('ghost.memoryDir must be relative'); - } - const segments = normalized.split('/'); - if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) { - throw new Error('ghost.memoryDir must not contain path traversal segments'); - } - return segments.join('/'); -} - -export async function readGhostPackageConfig(configPath: string): Promise { - const scan = await loadScanModule(); - const reader = scan.readOptionalPackageConfig; - if (typeof reader === 'function') { - return reader(configPath); - } - if (!existsSync(configPath) || !statSync(configPath).isFile()) return null; - const raw = await readFile(configPath, 'utf-8'); - return parseYaml(raw) as any; -} - -export async function resolveGhostRootCompat(input: { - root: string; - targetPath: string; - memoryDir: string; -}): Promise { - const scan = await loadScanModule(); - const newLoader = scan.loadFingerprintStackForPath; - const newContext = scan.fingerprintStackToPackageContext; - const newWriter = scan.writePackageContextBundleFromContext; - if ( - typeof newLoader === 'function' && - typeof newContext === 'function' && - typeof newWriter === 'function' - ) { - const rawStack = await newLoader(input.targetPath, input.root, { memoryDir: input.memoryDir }); - const rawContext = newContext(rawStack); - return normalizeScannerStack({ - rawStack, - rawContext, - writePrompt: () => writeScannerPrompt(rawContext, newWriter), - product: productFromNewStack(rawStack) ?? nameFromContext(rawContext), - memoryDir: stringAt(rawStack, 'fingerprint_dir') ?? input.memoryDir, - layerMemoryDirKey: 'fingerprint_dir', - }); - } - - const packageRoot = findNearestPackageRoot(input.root, input.targetPath, input.memoryDir); - if (packageRoot) { - return loadSplitPackageRoot(input.root, packageRoot, input.targetPath, input.memoryDir); - } - - const oldLoader = scan.loadMemoryStackForPath; - const oldContext = scan.memoryStackToPackageMemory; - const oldWriter = scan.writePackageContextBundleFromMemory; - if ( - typeof oldLoader === 'function' && - typeof oldContext === 'function' && - typeof oldWriter === 'function' - ) { - const rawStack = await oldLoader(input.targetPath, input.root, { memoryDir: input.memoryDir }); - const rawContext = oldContext(rawStack); - return normalizeScannerStack({ - rawStack, - rawContext, - writePrompt: () => writeScannerPrompt(rawContext, oldWriter), - product: productFromLegacyStack(rawStack) ?? nameFromContext(rawContext), - memoryDir: stringAt(rawStack, 'memory_dir') ?? input.memoryDir, - layerMemoryDirKey: 'memory_dir', - }); - } - - const legacyRoot = findNearestLegacyRoot(input.root, input.targetPath, input.memoryDir); - if (legacyRoot) { - return loadLegacyPackageRoot(input.root, legacyRoot, input.targetPath, input.memoryDir); - } - - throw new Error(`No ${input.memoryDir}/fingerprint/manifest.yml or ${input.memoryDir}/fingerprint.yml found`); -} - -async function writeScannerPrompt(rawContext: unknown, writer: unknown): Promise { - const dir = await makeTempDir(); - try { - await (writer as (context: unknown, opts: { outDir: string; promptOnly: boolean }) => Promise)( - rawContext, - { outDir: dir, promptOnly: true }, - ); - return await readFile(join(dir, 'prompt.md'), 'utf-8'); - } finally { - await rm(dir, { recursive: true, force: true }); - } -} - -async function loadSplitPackageRoot( - repoRoot: string, - packageRoot: string, - targetPath: string, - memoryDir: string, -): Promise { - const ghostRoot = join(packageRoot, memoryDir); - const fingerprintRoot = join(ghostRoot, 'fingerprint'); - const [manifestRaw, proseRaw, inventoryRaw, compositionRaw, checksRaw, intent] = await Promise.all([ - readRequired(join(fingerprintRoot, 'manifest.yml')), - readOptional(join(fingerprintRoot, 'prose.yml')), - readOptional(join(fingerprintRoot, 'inventory.yml')), - readOptional(join(fingerprintRoot, 'composition.yml')), - readOptional(join(fingerprintRoot, 'enforcement', 'checks.yml')), - readOptional(join(fingerprintRoot, 'memory', 'intent.md')), - ]); - const manifest = parseYaml(manifestRaw) as Record; - const prose = proseRaw ? parseYaml(proseRaw) as Record : {}; - const inventory = inventoryRaw ? parseYaml(inventoryRaw) as Record : {}; - const composition = compositionRaw ? parseYaml(compositionRaw) as Record : {}; - const product = stringAt(prose, 'summary', 'product') ?? stringAt(manifest, 'id') ?? 'Ghost'; - const relRoot = displayRelative(repoRoot, packageRoot); - const stack = stackForManualRoot({ - repoRoot, - targetPath, - memoryDir, - packageRoot, - relativeRoot: relRoot, - product, - }); - const context = { - manifest, - prose, - inventory, - composition, - checksRaw, - intent, - }; - return { - stack, - context: { - name: product, - raw: context, - writePrompt: () => promptForSplitPackage({ product, manifestRaw, proseRaw, inventoryRaw, compositionRaw, intent }), - }, - }; -} - -async function loadLegacyPackageRoot( - repoRoot: string, - packageRoot: string, - targetPath: string, - memoryDir: string, -): Promise { - const ghostRoot = join(packageRoot, memoryDir); - const [fingerprintRaw, checksRaw, intent] = await Promise.all([ - readRequired(join(ghostRoot, 'fingerprint.yml')), - readOptional(join(ghostRoot, 'checks.yml')), - readOptional(join(ghostRoot, 'intent.md')), - ]); - const fingerprint = parseYaml(fingerprintRaw) as Record; - const product = stringAt(fingerprint, 'summary', 'product') ?? 'Ghost'; - const relRoot = displayRelative(repoRoot, packageRoot); - return { - stack: stackForManualRoot({ - repoRoot, - targetPath, - memoryDir, - packageRoot, - relativeRoot: relRoot, - product, - }), - context: { - name: product, - raw: { fingerprint, checksRaw, intent }, - writePrompt: () => promptForLegacyPackage({ product, fingerprintRaw, checksRaw, intent }), - }, - }; -} - -function normalizeScannerStack(input: { - rawStack: any; - rawContext: any; - writePrompt: () => Promise; - product: string | null; - memoryDir: string; - layerMemoryDirKey: 'fingerprint_dir' | 'memory_dir'; -}): ResolvedGhostRootCompat { - const rawLayers = Array.isArray(input.rawStack?.layers) ? input.rawStack.layers : []; - const layers: GhostStackLayerCompat[] = rawLayers.map((layer: any): GhostStackLayerCompat => ({ - root: resolve(String(layer.root ?? input.rawStack.repo_root)), - relativeRoot: String(layer.relative_root ?? '.'), - memoryDir: String(layer[input.layerMemoryDirKey] ?? input.memoryDir), - })); - const provenanceLayers: GhostStackCompat['provenance']['layers'] = Array.isArray(input.rawStack?.provenance?.layers) - ? input.rawStack.provenance.layers.map((layer: any) => ({ - relativeRoot: String(layer.relative_root ?? '.'), - memoryDir: String(layer[input.layerMemoryDirKey] ?? layer.memory_dir ?? input.memoryDir), - })) - : layers.map((layer) => ({ relativeRoot: layer.relativeRoot, memoryDir: layer.memoryDir })); - return { - stack: { - repoRoot: resolve(String(input.rawStack.repo_root)), - targetPath: String(input.rawStack.target_path ?? '.'), - memoryDir: input.memoryDir, - layers, - provenance: { - merge: String(input.rawStack?.provenance?.merge ?? 'child-wins-by-id'), - layers: provenanceLayers, - }, - product: input.product, - raw: input.rawStack, - }, - context: { - name: nameFromContext(input.rawContext) ?? input.product ?? 'Ghost', - raw: input.rawContext, - writePrompt: input.writePrompt, - }, - }; -} - -function stackForManualRoot(input: { - repoRoot: string; - targetPath: string; - memoryDir: string; - packageRoot: string; - relativeRoot: string; - product: string | null; -}): GhostStackCompat { - return { - repoRoot: resolve(input.repoRoot), - targetPath: input.targetPath, - memoryDir: input.memoryDir, - layers: [{ - root: resolve(input.packageRoot), - relativeRoot: input.relativeRoot, - memoryDir: input.memoryDir, - }], - provenance: { - merge: 'child-wins-by-id', - layers: [{ relativeRoot: input.relativeRoot, memoryDir: input.memoryDir }], - }, - product: input.product, - raw: null, - }; -} - -async function promptForSplitPackage(input: { - product: string; - manifestRaw: string; - proseRaw: string | null; - inventoryRaw: string | null; - compositionRaw: string | null; - intent: string | null; -}): Promise { - return [ - `# ${input.product} Ghost Context`, - '', - 'Use this package-shaped Ghost fingerprint as product-experience memory for generation.', - '', - '## Manifest', - codeBlock(input.manifestRaw), - input.proseRaw ? `## Prose\n\n${codeBlock(input.proseRaw)}` : '', - input.inventoryRaw ? `## Inventory\n\n${codeBlock(input.inventoryRaw)}` : '', - input.compositionRaw ? `## Composition\n\n${codeBlock(input.compositionRaw)}` : '', - input.intent ? `## Human-Approved Intent\n\n${input.intent.trim()}` : '', - ].filter(Boolean).join('\n\n'); -} - -async function promptForLegacyPackage(input: { - product: string; - fingerprintRaw: string; - checksRaw: string | null; - intent: string | null; -}): Promise { - return [ - `# ${input.product} Ghost Context`, - '', - 'Use this legacy Ghost fingerprint.yml as compatibility product-experience memory for generation.', - '', - '## Fingerprint', - codeBlock(input.fingerprintRaw), - input.checksRaw ? `## Checks\n\n${codeBlock(input.checksRaw)}` : '', - input.intent ? `## Human-Approved Intent\n\n${input.intent.trim()}` : '', - ].filter(Boolean).join('\n\n'); -} - -function codeBlock(value: string): string { - return `\`\`\`yaml\n${value.trim()}\n\`\`\``; -} - -function findNearestPackageRoot(root: string, targetPath: string, memoryDir: string): string | null { - return findNearestRootWith(root, targetPath, join(memoryDir, 'fingerprint', 'manifest.yml')); -} - -function findNearestLegacyRoot(root: string, targetPath: string, memoryDir: string): string | null { - return findNearestRootWith(root, targetPath, join(memoryDir, 'fingerprint.yml')); -} - -function findNearestRootWith(root: string, targetPath: string, relativeNeedle: string): string | null { - const repoRoot = resolve(root); - let cursor = resolve(repoRoot, targetPath); - if (existsSync(cursor) && statSync(cursor).isFile()) cursor = dirname(cursor); - while (isWithinOrEqual(repoRoot, cursor)) { - if (existsSync(join(cursor, relativeNeedle))) return cursor; - const parent = dirname(cursor); - if (parent === cursor) break; - cursor = parent; - } - return null; -} - -async function readOptional(path: string): Promise { - try { - return await readFile(path, 'utf-8'); - } catch { - return null; - } -} - -async function readRequired(path: string): Promise { - return readFile(path, 'utf-8'); -} - -async function makeTempDir(): Promise { - const dir = join(tmpdir(), `summon-ghost-context-${Date.now()}-${Math.random().toString(36).slice(2)}`); - await mkdir(dir, { recursive: true }); - return dir; -} - -function productFromNewStack(stack: any): string | null { - return stringAt(stack, 'merged', 'fingerprint', 'prose', 'summary', 'product'); -} - -function productFromLegacyStack(stack: any): string | null { - return stringAt(stack, 'merged', 'fingerprint', 'summary', 'product'); -} - -function nameFromContext(context: any): string | null { - return typeof context?.name === 'string' && context.name.trim() ? context.name.trim() : null; -} - -function stringAt(obj: any, ...path: string[]): string | null { - let value = obj; - for (const key of path) { - value = value?.[key]; - } - return typeof value === 'string' && value.trim() ? value.trim() : null; -} - -function displayRelative(root: string, path: string): string { - const rel = relative(resolve(root), resolve(path)); - return rel && !rel.startsWith('..') && !isAbsolute(rel) ? rel || '.' : '.'; -} - -function isWithinOrEqual(root: string, child: string): boolean { - const rel = relative(resolve(root), resolve(child)); - return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); -} diff --git a/apps/server/src/infer-capabilities.ts b/apps/server/src/infer-capabilities.ts deleted file mode 100644 index 7517987..0000000 --- a/apps/server/src/infer-capabilities.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { CapabilityPack } from '@anarchitecture/summon'; -import type { TextCompletionClient } from './model-providers.js'; - -export interface InferenceResult { - /** Narrowed pack — never wider than the ceiling. Null when mode is static. */ - pack: CapabilityPack | null; - mode: 'static' | 'interactive'; -} - -/** - * Use a utility model to decide whether a prompt needs interactivity, and if so - * which subset of the ceiling pack's intents it actually requires. Returns - * null on timeout or error so the caller can fall through to the regex. - * - * The classifier is constrained: it can only NARROW the intent list, never - * add intents the host didn't declare. This preserves the strict-tier - * contract — a host that locked its pack to a single intent stays locked. - * - * The default timeout is intentionally small: callers should trade uncertain - * classification for a regex fallback rather than delay the main stream. - */ -export async function inferPack( - client: TextCompletionClient, - prompt: string, - ceiling: CapabilityPack, - timeoutMs = 2000 -): Promise { - if (ceiling.intents.length === 0) { - return { pack: null, mode: 'static' }; - } - - const intentList = ceiling.intents - .map((i) => `- ${i.name} [${i.kind === 'resource' ? 'data resource' : 'action'}]: ${i.description || '(no description)'}`) - .join('\n'); - - const systemText = `Decide whether a UI generation prompt needs interactivity, and if so which action intents and data resources from a fixed flat list it actually requires. - -Available actions and data resources: -${intentList} - -Respond with ONLY a single JSON object on one line. No markdown fences, no prose. Shape: -{"mode":"static","intents":[]} -or -{"mode":"interactive","intents":["intent_name", ...]} - -Use "static" when the prompt asks for content (cards, articles, recommendations, comparisons, dashboards, summaries) without user interaction. Use "interactive" when the prompt clearly asks for the user to act — pick, submit, filter, toggle, search, vote, track, etc. Also use "interactive" when the UI needs host-owned data from a listed data resource. - -When interactive, include ONLY the names the prompt actually needs. A picker prompt usually needs the "choose" action alone, not "submit". A search prompt needs the "search" data resource. Never include names that aren't in the available list above.`; - - try { - const callPromise = client.completeText({ - system: systemText, - prompt, - maxTokens: 200, - }); - - const result = await Promise.race([ - callPromise, - new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)), - ]); - - if (!result) return null; - - const raw = result.trim(); - - // Tolerate fenced JSON or leading prose. - const cleaned = raw - .replace(/^[\s\S]*?```(?:json)?\s*/i, '') - .replace(/\s*```[\s\S]*$/, '') - .trim(); - const candidate = cleaned.startsWith('{') ? cleaned : raw; - const match = candidate.match(/\{[\s\S]*\}/); - const json = match ? match[0] : null; - if (!json) return null; - - let parsed: { mode?: unknown; intents?: unknown }; - try { - parsed = JSON.parse(json); - } catch { - return null; - } - - const mode = parsed.mode === 'interactive' ? 'interactive' : 'static'; - if (mode === 'static') return { pack: null, mode }; - - const requested = Array.isArray(parsed.intents) - ? new Set(parsed.intents.filter((x): x is string => typeof x === 'string')) - : new Set(); - const narrowed = ceiling.intents.filter((i) => requested.has(i.name)); - - if (narrowed.length === 0) { - // The classifier said interactive but produced no usable intents — treat as - // ambiguous and let the caller fall through to the regex. - return null; - } - - return { - mode, - pack: { intents: narrowed, patterns: ceiling.patterns }, - }; - } catch (err) { - console.error('[infer-capabilities] error:', err instanceof Error ? err.message : err); - return null; - } -} diff --git a/apps/server/src/infer-shape.ts b/apps/server/src/infer-shape.ts index 6660f09..cd3b1fd 100644 --- a/apps/server/src/infer-shape.ts +++ b/apps/server/src/infer-shape.ts @@ -5,8 +5,10 @@ import type { TextCompletionClient } from './model-providers.js'; * 1:1 to the shape exemplars a direction may carry — picking a shape selects * the matching exemplar to ship in the per-direction prompt block. * - * Shapes that don't have a dedicated exemplar (plan/itinerary, reflection) - * collapse onto the closest match: "plan" → article, "reflection" → card. + * Shapes that don't have a dedicated exemplar should collapse onto the richest + * structural match: plan/itinerary/reflection → article, status metrics → + * tracker, side-by-side choices → comparison. The "card" shape is intentionally + * narrow for a single focused brief or object profile. * The classifier emits null when ambiguous; the caller then ships all shape * exemplars (current behavior) instead of guessing wrong. */ @@ -29,15 +31,15 @@ export async function inferShape( const systemText = `Classify a generative-UI prompt into the response shape that best fits the user's intent. Shapes: -- article — long-form explainer, walkthrough, plan/itinerary. Lead is body copy under a heading. -- card — focused single-block summary, recommendation, status readout, weekly digest. -- comparison — side-by-side options, decision support, A vs B, pros/cons. -- tracker — dashboard, dominant number with breakdown, progress, stats grid. +- article — long-form explainer, walkthrough, plan/itinerary, reflection, memo, guide, or readable summary. +- card — single focused brief, recommendation, object profile, or compact standalone answer. Use only when the response is truly one bounded object. +- comparison — side-by-side options, decision support, A vs B, pros/cons, matrix, table, or verdict. +- tracker — dashboard, status readout, weekly digest, dominant number with breakdown, progress, operational queue, or stats grid. Respond with ONLY a single JSON object on one line. No markdown fences, no prose. Shape: {"shape":"article"} or {"shape":"card"} or {"shape":"comparison"} or {"shape":"tracker"} or {"shape":null} -Use null ONLY when the prompt genuinely fits none of the above OR when two shapes are equally plausible. When in doubt between two shapes, pick the more specific one (comparison > article, tracker > card).`; +Use null ONLY when the prompt genuinely fits none of the above OR when two shapes are equally plausible. Avoid using "card" as a fallback for summaries, dashboards, recaps, or multi-part briefs; prefer article, tracker, or comparison. When in doubt between two shapes, pick the more specific one (comparison > tracker > article > card).`; try { const callPromise = client.completeText({ diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 7bb0638..e57ca3d 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -26,13 +26,11 @@ import { fileURLToPath } from 'node:url'; import { defaultDirectionId, loadDirections, - PREFERRED_DEFAULT_DIRECTION_ID, type Direction, } from './directions-loader.js'; import { registerDemoRoutes } from './demo-routes.js'; import { buildGhostReviewPacket, - ghostCapsuleMeta, ghostContextMeta, ghostTokenSourceMeta, parseGhostRequest, @@ -42,7 +40,6 @@ import { resolveGhostGenerationContext, type ResolvedGhostSteer, } from './ghost-adapter.js'; -import { inferPack } from './infer-capabilities.js'; import { inferShape, type ResponseShape } from './infer-shape.js'; import { parseCapabilityPack } from './capability-pack.js'; import { parseComponentPack } from './component-pack.js'; @@ -73,6 +70,38 @@ const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'http://localhost:5173'; const modelProviders = createModelProviderRegistry(process.env); const defaultModelProvider = modelProviders.defaultProvider; +const EXPERIMENTAL_BLOCK_FRAGMENT_PROMPT = `## Experimental block fragments + +This run is using Summon's experimental block-fragment protocol. Keep sections as the outer structure, but stream complete blocks inside each section. + +Rules: + +- Emit \`set /screen\` first with the stable section ids. +- For each section, emit \`set /section/\` with \`{"blocks":["block-id"]}\` before adding blocks. +- Emit complete block replacement lines at \`add /section//block/\`. +- Use lowercase kebab-case ids. Each section may declare 1 to 8 blocks. +- Treat every block as a complete subtree. Do not split a form, table, data resource scope, component placeholder, script lifecycle, or closely coupled control group across blocks. +- For perceived streaming, emit cheap block placeholders first, then final replacement \`add\` lines for the same block ids. +- Do not emit whole-section \`add /section/\` lines unless you cannot satisfy the block contract.`; + +const EXPERIMENTAL_HTML_NODE_PROMPT = `## Experimental HTML node patches + +This run is using Summon's experimental html-node-v0 protocol. Keep sections as the outer structure, but stream small complete raw-HTML DOM nodes inside each section. + +Rules: + +- Emit \`set /screen\` first with stable section ids. +- Then emit \`add /section//node/\` lines. Each node line must include one complete raw HTML element with \`data-summon-node=""\` on that root element. +- Use lowercase kebab-case ids for sections and nodes. +- Omit \`parent\` to append a node directly under the section wrapper. Set \`parent\` to an earlier node id to append inside that parent node. +- Emit a root composition container first, then useful child nodes such as headers, table sections, rows, list items, timeline steps, action groups, chart shells, callouts, notes, and metric blocks when appropriate. +- Emit useful shells early. When a purposeful container, list, table, timeline, or chart shell will receive child node patches, include a child slot inside it, such as \`
\`, and set those child lines' \`parent\` to that shell's node id. +- Shell slots may include 1-3 direct lightweight placeholders with \`data-summon-skeleton\`; later real child node patches will replace those placeholders automatically. +- Do not orphan content that belongs inside a declared parent container. Choose parent containers because the layout needs them, not as decoration. +- Each node patch should usually be 500-2000 bytes and visually meaningful immediately. Prefer many small useful patches over one large section. +- Do not put \`data-summon-node\` on nested elements inside a node patch. Child nodes must arrive as their own later protocol lines. +- Do not emit scripts, inline event handlers, external URLs, or whole-section \`add /section/\` lines unless you cannot satisfy the node-patch contract.`; + if (!defaultModelProvider) { console.error( '[summon-server] no model provider is configured. Copy apps/server/.env.example to .env and set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY.' @@ -296,23 +325,6 @@ function clampInt(value: unknown, min: number, max: number, fallback: number): n return Math.max(min, Math.min(max, Math.floor(value))); } -/** - * Conservative two-signal heuristic for "this static-mode prompt actually wants - * interactivity". Requires BOTH an interactive verb AND user-action framing, - * so passive list requests like "pick a name for my puppy" don't trip it. - * - * Tunable knob: tighten ACTION_FRAMING for false-positives; loosen for - * false-negatives. - */ -const INTERACTIVE_VERBS = - /\b(pick|choose|select|submit|filter|toggle|track|count|tally|vote|rate|swipe|drag|search\s+for|let\s+me\s+(pick|choose|search|filter|select))\b/i; -const ACTION_FRAMING = - /\b(let\s+me|i\s+can|lets?\s+the\s+user|so\s+i\s+can|that\s+i\s+can|where\s+i\s+can|i\s+(want\s+to|need\s+to)\s+(pick|choose|select|filter|track|count|search|toggle)|with\s+\d+\s+(options?|choices?|cards?|tabs?)|from\s+\d+\s+(options?|choices?|cards?|tabs?)|help\s+me\s+(pick|choose|select|filter|find|track|count|toggle))\b/i; - -function detectsInteractiveIntent(prompt: string): boolean { - return INTERACTIVE_VERBS.test(prompt) && ACTION_FRAMING.test(prompt); -} - // Simple concurrency cap for /api/generate — protects against a runaway batch // page firing too many parallel streams at the selected model API. const MAX_CONCURRENT_GENERATIONS = 12; @@ -374,9 +386,7 @@ app.get('/api/ghost-roots', (_req, res) => { publicGhostRoots(ghostRoots).map(({ id }) => ({ id, defaultTargetPath: '.', - defaultBaseDirectionId: directionsById.has(PREFERRED_DEFAULT_DIRECTION_ID) - ? PREFERRED_DEFAULT_DIRECTION_ID - : DEFAULT_DIRECTION_ID ?? null, + defaultBaseDirectionId: null, })), ); }); @@ -410,19 +420,19 @@ app.post('/api/generate', async (req, res) => { req.body?.tokenOverrides !== undefined && req.body.tokenOverrides !== null ) { - res.status(400).json({ error: 'tokenOverrides are not supported with Ghost product memory' }); + res.status(400).json({ error: 'tokenOverrides are not supported with Ghost fingerprints' }); return; } const requestedGhostBaseDirectionId = ghostRequest - ? (ghostRequest.baseDirectionId ?? (directionsById.has(PREFERRED_DEFAULT_DIRECTION_ID) ? PREFERRED_DEFAULT_DIRECTION_ID : null)) + ? ghostRequest.baseDirectionId : null; const ghostBaseDirection = requestedGhostBaseDirectionId ? directionsById.get(requestedGhostBaseDirectionId) : undefined; if (ghostRequest && requestedGhostBaseDirectionId && !ghostBaseDirection) { - res.status(400).json({ error: `unknown Ghost base direction "${requestedGhostBaseDirectionId}"` }); + res.status(400).json({ error: `unknown fingerprint token fallback direction "${requestedGhostBaseDirectionId}"` }); return; } @@ -443,11 +453,11 @@ app.post('/api/generate', async (req, res) => { } const directionId = ghostContext - ? ghostContext.baseDirectionId ?? undefined + ? undefined : ((typeof req.body?.directionId === 'string' ? req.body.directionId : undefined) ?? DEFAULT_DIRECTION_ID); const direction = ghostContext - ? ghostBaseDirection + ? undefined : directionId ? directionsById.get(directionId) : undefined; @@ -467,15 +477,22 @@ app.post('/api/generate', async (req, res) => { return; } const edit = parsedEdit.edit; + const fragmentMode = + req.body?.fragmentMode === 'block-v0' + ? 'block-v0' + : req.body?.fragmentMode === 'html-node-v0' + ? 'html-node-v0' + : 'section'; const repairOptions = parseRepairOptions(req.body?.repair); const rawAgentOptions = req.body?.agent; const agentOptions = rawAgentOptions && typeof rawAgentOptions === 'object' ? rawAgentOptions as Record : null; - const agentPlanningEnabled = agentOptions?.enabled === true; const hasSurfacePolicy = req.body?.surfacePolicy !== undefined && req.body.surfacePolicy !== null; + const hasSurfacePlan = + req.body?.surfacePlan !== undefined && req.body.surfacePlan !== null; const requestedMode: 'static' | 'interactive' = req.body?.mode === 'interactive' ? 'interactive' : 'static'; let scriptPolicy: ScriptPolicy | undefined = @@ -486,7 +503,6 @@ app.post('/api/generate', async (req, res) => { let mode: 'static' | 'interactive' = requestedMode; let pack: CapabilityPack | null = null; let modeUpgraded = false; - let inferenceUsed = false; let surfacePlan: SurfacePlan; let agentPlan: AgentSurfacePlanResult | null = null; @@ -497,7 +513,7 @@ app.post('/api/generate', async (req, res) => { // are no exemplars to filter. Also skipped when the host supplies a layout: // the layout is the composition anchor, and exemplars become visual-only. let shape: ResponseShape | null = null; - if (!layout && direction && process.env.SUMMON_INFER_SHAPE !== '0') { + if (!layout && (direction || ghostContext) && process.env.SUMMON_INFER_SHAPE !== '0') { shape = await inferShape({ completeText: (request) => modelProvider.completeText(request, modelSelection), }, prompt); @@ -512,7 +528,24 @@ app.post('/api/generate', async (req, res) => { scriptPolicy = compiledPolicy.scriptPolicy; pack = compiledPolicy.capabilities; surfacePlan = compiledPolicy.surfacePlan; - } else if (agentPlanningEnabled) { + } else if (hasSurfacePlan) { + pack = requestedMode === 'interactive' ? capabilityCeiling : null; + const resolvedSurface = resolveSurfaceGenerationPlan({ + prompt, + mode, + scriptPolicy, + capabilities: pack, + rawSurfacePlan: req.body?.surfacePlan, + rawSurfaceCeiling: req.body?.surfaceCeiling, + }); + if (mode !== resolvedSurface.mode) { + modeUpgraded = mode === 'static' && resolvedSurface.mode === 'interactive' ? true : modeUpgraded; + mode = resolvedSurface.mode; + pack = mode === 'interactive' ? pack ?? capabilityCeiling : null; + } + scriptPolicy = resolvedSurface.scriptPolicy; + surfacePlan = resolvedSurface.surfacePlan; + } else { agentPlan = await planAgentSurface({ prompt, capabilities: capabilityCeiling, @@ -529,56 +562,6 @@ app.post('/api/generate', async (req, res) => { pack = agentPlan.compiledPolicy.capabilities; surfacePlan = agentPlan.compiledPolicy.surfacePlan; modeUpgraded = requestedMode === 'static' && mode === 'interactive'; - } else { - // Layer 3: utility-model capability inference. Decides mode + narrows the - // pack to the minimal subset of intents the prompt actually needs. The - // pack is treated as a ceiling — inference can only narrow, never expand. - // Falls through to the Layer 2 regex on timeout or error. - if (process.env.SUMMON_INFER_CAPABILITIES === '1' && capabilityCeiling) { - const inferred = await inferPack({ - completeText: (request) => modelProvider.completeText(request, modelSelection), - }, prompt, capabilityCeiling); - if (inferred) { - inferenceUsed = true; - if (requestedMode === 'interactive') { - // Respect the user's explicit interactive choice. Inference may narrow - // the pack, but won't downgrade to static. - mode = 'interactive'; - pack = inferred.pack ?? capabilityCeiling; - } else { - mode = inferred.mode; - pack = inferred.pack; - modeUpgraded = mode === 'interactive'; - } - } - } - - // Layer 2 regex fallback — runs when inference is disabled, ceiling is - // missing, or inference returned null (timeout/parse failure). Only upgrades - // when a ceiling exists; without one there's no Capabilities block to emit. - if (!inferenceUsed) { - if (requestedMode === 'static' && capabilityCeiling && detectsInteractiveIntent(prompt)) { - mode = 'interactive'; - modeUpgraded = true; - } - pack = mode === 'interactive' ? capabilityCeiling : null; - } - - const resolvedSurface = resolveSurfaceGenerationPlan({ - prompt, - mode, - scriptPolicy, - capabilities: pack, - rawSurfacePlan: req.body?.surfacePlan, - rawSurfaceCeiling: req.body?.surfaceCeiling, - }); - if (mode !== resolvedSurface.mode) { - modeUpgraded = mode === 'static' && resolvedSurface.mode === 'interactive' ? true : modeUpgraded; - mode = resolvedSurface.mode; - pack = mode === 'interactive' ? pack ?? capabilityCeiling : null; - } - scriptPolicy = resolvedSurface.scriptPolicy; - surfacePlan = resolvedSurface.surfacePlan; } if (ghostContext) { @@ -613,13 +596,6 @@ app.post('/api/generate', async (req, res) => { path: '/ghost-token-source', value: ghostTokenSourceMeta(ghostContext.tokenSource), }); - if (ghostContext.source === 'root' && ghostContext.capsule) { - preludeLines.push({ - op: 'meta', - path: '/ghost-capsule', - value: ghostCapsuleMeta(ghostContext.capsule), - }); - } } // Emit the mode-upgrade signal before agent diagnostics. The client respawns // its sandbox into interactive mode in response, so this should land before @@ -638,6 +614,7 @@ app.post('/api/generate', async (req, res) => { path: '/agent-policy-resolution', value: { source: agentPlan.policyResolution.source, + intentSource: agentPlan.intentSource, proposedSurfacePolicy: agentPlan.policyResolution.proposedSurfacePolicy, surfacePolicy: agentPlan.policyResolution.surfacePolicy, rejectedCapabilities: agentPlan.policyResolution.rejectedCapabilities, @@ -652,6 +629,13 @@ app.post('/api/generate', async (req, res) => { if (layout) { preludeLines.push({ op: 'meta', path: '/layout', value: layout.id }); } + if (fragmentMode !== 'section' && !edit) { + preludeLines.push({ + op: 'meta', + path: '/experimental-fragments', + value: { mode: fragmentMode }, + }); + } if (edit) { preludeLines.push({ op: 'meta', @@ -698,8 +682,19 @@ app.post('/api/generate', async (req, res) => { } : null, ghost: ghostContext ?? null, + activeTokensCss: ghostContext?.tokenSource.css ?? null, layout, edit, + experimentalPromptBlock: fragmentMode !== 'section' && !edit + ? { + id: `experimental-fragments:${fragmentMode}`, + text: fragmentMode === 'html-node-v0' + ? EXPERIMENTAL_HTML_NODE_PROMPT + : EXPERIMENTAL_BLOCK_FRAGMENT_PROMPT, + cache: 'ephemeral', + } + : null, + experimentalFragmentMode: !edit ? fragmentMode : 'section', capabilities: hasSurfacePolicy || agentPlan ? capabilityCeiling : pack, components: componentPack, surfacePolicy: hasSurfacePolicy @@ -710,7 +705,6 @@ app.post('/api/generate', async (req, res) => { scriptPolicy: hasSurfacePolicy || agentPlan ? undefined : scriptPolicy, surfacePlan: hasSurfacePolicy || agentPlan ? null : surfacePlan, tokenOverrides: overrides.applied, - activeTokensCss: ghostContext?.tokenSource.css ?? direction?.tokensCss ?? null, preludeLines, repair, modelProvider: (request) => modelProvider.streamSurfaceGeneration(request, (nextUsage) => { @@ -742,7 +736,7 @@ app.post('/api/generate', async (req, res) => { cache_creation_input_tokens: 0, }; const stats = summary.repairStats ?? { queued: 0, cancelled: 0, repaired: 0, failed: 0 }; - const upgradeTag = modeUpgraded ? ` (upgraded ${inferenceUsed ? 'via inference' : 'via regex'})` : ''; + const upgradeTag = modeUpgraded ? ` (upgraded via ${agentPlan ? 'broker' : 'surface plan'})` : ''; console.log( `[generate] provider=${modelProvider.id}/${modelSelection.generationModel} utility=${modelSelection.utilityModel} dir=${directionId ?? 'none'} ghost=${ghostContext ? ghostLogId(ghostContext) : 'none'} mode=${mode}${upgradeTag}` + ` shape=${shape ?? 'all'}` + @@ -775,9 +769,7 @@ app.post('/api/generate', async (req, res) => { }); function ghostLogId(context: ResolvedGhostSteer): string { - return context.source === 'root' - ? context.request.rootId - : (context.request.id ?? 'resolved-context'); + return context.request.rootId; } app.listen(PORT, () => { diff --git a/apps/server/src/model-providers.ts b/apps/server/src/model-providers.ts index 8530379..0337ecd 100644 --- a/apps/server/src/model-providers.ts +++ b/apps/server/src/model-providers.ts @@ -461,7 +461,7 @@ function parseEffort(raw: unknown, fallback: ModelEffort): ModelEffort { function createAnthropicProvider(env: NodeJS.ProcessEnv): ModelProviderAdapter { const apiKey = env.ANTHROPIC_API_KEY?.trim(); - const model = env.ANTHROPIC_MODEL ?? 'claude-sonnet-4-6'; + const model = env.ANTHROPIC_MODEL ?? 'claude-opus-4-8'; const utilityModel = env.ANTHROPIC_SMALL_MODEL ?? 'claude-haiku-4-5'; const defaults = createProviderDefaults({ env, @@ -831,7 +831,7 @@ function promptBlocksToText(blocks: ContractPromptBlock[]): string { } function repairModeSystemText(): string { - return '## Repair mode\n\nYou are repairing one blocked Summon section. Return exactly one safe replacement `add /section/` JSONL line and nothing else.'; + return '## Repair mode\n\nYou are repairing one blocked Summon target. Return exactly one safe replacement `add` JSONL line for the same target path and nothing else.'; } function extractAnthropicText(content: Anthropic.Message['content']): string { diff --git a/apps/server/src/surface-plan.test.ts b/apps/server/src/surface-plan.test.ts index 77aa01b..e3bf4a6 100644 --- a/apps/server/src/surface-plan.test.ts +++ b/apps/server/src/surface-plan.test.ts @@ -157,7 +157,7 @@ test('parsed worker capability resolves to worker surface when explicitly reques }); }); -test('scripted surface resolves to allow only when explicitly requested within ceiling', () => { +test('legacy scripted surface plan falls back to declarative defaults', () => { const resolved = resolveSurfaceGenerationPlan({ prompt: 'build keyboard shortcuts with local highlighted selection', mode: 'interactive', @@ -177,19 +177,19 @@ test('scripted surface resolves to allow only when explicitly requested within c }, }); - assert.equal(resolved.explicitAccepted, true); - assert.equal(resolved.source, 'explicit'); - assert.equal(resolved.scriptPolicy, 'allow'); + assert.equal(resolved.explicitAccepted, false); + assert.equal(resolved.source, 'default'); + assert.equal(resolved.scriptPolicy, 'forbid'); assert.deepEqual(resolved.surfacePlan, { - purpose: 'explore', - runtime: 'scripted', + purpose: 'inform', + runtime: 'declarative', data: 'embedded', - authority: 'host-action', + authority: 'none', persistence: 'replayable', }); }); -test('scripted request falls back to forbid when ceiling excludes scripted runtime', () => { +test('legacy script allow falls back to forbid when plan is invalid', () => { const resolved = resolveSurfaceGenerationPlan({ prompt: 'build keyboard shortcuts with local highlighted selection', mode: 'interactive', diff --git a/docs/adoption/debugging.md b/docs/adoption/debugging.md index c04641e..4962006 100644 --- a/docs/adoption/debugging.md +++ b/docs/adoption/debugging.md @@ -7,7 +7,7 @@ needed. ## Generation Failed -Open the **Stream** drawer on `/generate.html` and check: +Open the **Stream** drawer on `/generate` and check: - `/error` - server-side generation error or blocked-generation message. - `/validation-blocked` - a blocking issue stopped generation or validation @@ -21,10 +21,12 @@ Common fixes: - `external-url` - inline assets as data URLs or remove the reference. - `unsafe-tag` - remove iframe, object, embed, link, meta, or base-like tags. -- `inline-handler` - use `data-summon-on-*` or scoped `addEventListener`. +- `inline-handler` - use `data-summon-on-*`, `data-summon-set`, or + `data-summon-toggle`. - `static-script` - remove scripts or choose an interactive surface config. -- `script-not-granted` - use only declarative `data-summon-*` bindings, or - select `SurfacePolicy.tier: "scripted"` for a scripted surface type. +- `script-not-granted` / `surface-script-policy-removed` - use declarative + `data-summon-*` bindings, local state, and motion primitives instead of + generated scripts. - `surface-policy-*` - fix the host-selected surface config. The compiler blocks unknown allowed tools/components and authority above the selected surface type before the model is called. @@ -109,7 +111,7 @@ Common fixes: 1. Run `pnpm test:safety`. 2. If it fails, inspect the Playwright trace/screenshot. 3. For manual inspection, run `pnpm dev:all` and open - `http://localhost:5173/adversarial.html`. + `http://localhost:5173/adversarial`. 4. Confirm network, storage, parent DOM, and unallowed host tool request checks still pass. 5. Inspect `spawnSandbox` before changing iframe sandbox attributes or CSP. @@ -139,6 +141,8 @@ These names are useful when maintaining Summon or writing a deeper adapter: | Path | Meaning | | --- | --- | +| `/agent-intent` | Broker-advisory intent inferred from the prompt before host policy narrowing. | +| `/agent-policy-resolution` | Brokered proposed/effective surface config, host policy source, intent source, and rejected tools/components. | | `/surface-policy` | Host-owned public surface config selected for this run. | | `/surface-plan` | Host-owned compiled safety plan selected for this run. | | `/surface-contract` | Host-owned compact view of the selected policy, narrowed tools/resources, trusted components, optional layout, and compile issues. | diff --git a/docs/adoption/integration.md b/docs/adoption/integration.md index 6b381d9..29e6daa 100644 --- a/docs/adoption/integration.md +++ b/docs/adoption/integration.md @@ -209,11 +209,13 @@ await runAgentSurfaceGeneration({ ``` The broker emits `/agent-intent` and `/agent-policy-resolution` diagnostics, -then generation continues through the normal `/surface-policy`, -`/surface-plan`, and `/surface-contract` path. `SurfaceIntent` is an -experimental planning shape, not an authority contract. The inferred intent is -advisory: the host resolver and `compileSurfacePolicy()` still decide which -tools, components, runtime, and approval paths are actually available. +including whether the intent came from a provided value, model classifier, or +deterministic fallback. Generation then continues through the normal +`/surface-policy`, `/surface-plan`, and `/surface-contract` path. +`SurfaceIntent` is an experimental planning shape, not an authority contract. +The inferred intent is advisory: the host resolver and `compileSurfacePolicy()` +still decide which tools, components, runtime, and approval paths are actually +available. ### Surface Contract View diff --git a/docs/adoption/package-consumption.md b/docs/adoption/package-consumption.md index 2181885..d94869c 100644 --- a/docs/adoption/package-consumption.md +++ b/docs/adoption/package-consumption.md @@ -90,6 +90,7 @@ defineReactComponent({ ```ts import { + compileArtifactHtml, compileSurfaceContractView, compileSurfacePolicy, createComponentRegistry, @@ -110,9 +111,9 @@ import { ``` Use `consumeSurfaceStream()` to decode streamed chunks, parse accepted protocol -lines, maintain generated HTML, update stream diagnostics, and render through -the sandbox handle. Spawn the iframe with allowed host tools from host-owned -contracts. +lines, compile generated HTML when validation context is available, maintain +generated HTML, update stream diagnostics, and render through the sandbox +handle. Spawn the iframe with allowed host tools from host-owned contracts. `compileSurfacePolicy(surfacePolicy, catalogs)` gives the client the stream mode and narrowed contracts that the server will enforce. Generation authority @@ -152,11 +153,20 @@ const policy = new PolicyEngine({ handlers, onStateChange: (state) => handle?.pushState(state), }); +const validationContext = { + mode: compiledPolicy.mode, + scriptPolicy: compiledPolicy.scriptPolicy, + allowedIntents: policy.intents, + capabilities: grantedCapabilities, + components: componentContract.validationComponents, + surfacePlan: compiledPolicy.surfacePlan, +}; +const blankHtml = compileArtifactHtml('', validationContext).html; handle = spawnSandbox({ iframe, artifact: { - html: '', + html: blankHtml, intents: policy.intents, capabilities: grantedCapabilities, components: componentContract.validationComponents, @@ -187,6 +197,7 @@ const response = await fetch('/api/generate', { await consumeSurfaceStream(response.body!, { mode: compiledPolicy.mode, + validationContext, onRenderHtml: (html) => handle?.render(html), }); ``` diff --git a/docs/adoption/quickstart.md b/docs/adoption/quickstart.md index e3a1ecb..1ea9447 100644 --- a/docs/adoption/quickstart.md +++ b/docs/adoption/quickstart.md @@ -38,19 +38,21 @@ generation and replay. pnpm dev:all ``` -Open `http://localhost:5173/generate.html`. +Open `http://localhost:5173/generate`. -The Generate workbench uses the same surface configs as the gallery where they -overlap, but keeps maintainer controls visible: stream diagnostics, Devtools, +The Generate workbench runs showcase prompts through the agent broker by +default, then keeps maintainer controls visible: stream diagnostics, Devtools, validation retry, edit/replay, custom SurfacePlan overrides, directions, and -Ghost steering internals. +Ghost fingerprint steering internals. The custom Surface Config panel is the explicit +manual override path. ## Run A Ghost Sandbox The Surface Gallery and Generate workbench both add Ghost-backed sandbox presets -when trusted roots are configured. The bundled **Ghost** direction is a visual -direction snapshot; root-backed product memory is enabled separately so the -host still owns which repositories the model can read from. +when trusted roots are configured. A configured Ghost root is treated as a +fingerprint package, not as a bundled visual direction. Ghost resolves the +product design context; Summon still owns host policy, capabilities, runtime +contracts, and token fallback. Add one or more trusted Ghost roots to `apps/server/.env`: @@ -71,9 +73,9 @@ Canonical Ghost packages use this layout: └── composition.yml ``` -Legacy roots with `.ghost/fingerprint.yml` are still accepted through the -compatibility bridge, but new examples and product fingerprints should use -`.ghost/fingerprint/manifest.yml`. +Only canonical split fingerprint packages are supported. Roots must include +`.ghost/fingerprint/manifest.yml`; legacy `.ghost/fingerprint.yml` files are +not accepted. Then start the gallery or the workbench: @@ -84,17 +86,25 @@ pnpm dev:all ``` Open `http://localhost:5174` for the adopter-facing gallery preset, or -`http://localhost:5173/generate.html` for the diagnostic Ghost scenario and -`Ghost · ` direction. Keep **Ghost base** on the bundled **Ghost** direction -unless you are intentionally testing another token base. **Ghost target** is a -relative path inside the configured repo root; use `.` for the root package or a -nested surface path. +`http://localhost:5173/generate` for the diagnostic fingerprint scenario and +`Fingerprint · ` option. **Fingerprint target** is a relative path inside +the configured repo root; use `.` for the root package or a nested surface path. +**Token fallback** is optional and only supplies CSS tokens when the fingerprint +package does not provide contract-complete tokens. When the run starts, the Stream drawer should show `/ghost-context`, `/ghost-token-source`, and `/ghost-review-packet` metadata. Those lines confirm -the server resolved the fingerprint stack, chose token CSS, generated a Summon +Ghost relay resolved the fingerprint stack, Summon chose token CSS, generated a surface, and emitted the review packet needed to inspect the output against the -same Ghost memory. +same Ghost fingerprint. + +Useful checks for a configured root: + +```sh +ghost lint +ghost verify . --root . +ghost relay gather . +``` ## Golden Scenario @@ -119,7 +129,7 @@ shaped to exercise the adopter path: 7. Open **Saved surfaces** and replay the completed surface. It should render the same UI while keeping the sandbox boundary intact. -Then open `http://localhost:5173/adversarial.html` and confirm the sandbox +Then open `http://localhost:5173/adversarial` and confirm the sandbox checks pass. This proves the quickstart did not require relaxing the sandbox boundary. @@ -129,7 +139,8 @@ The Stream and Devtools drawers are for understanding a run after you have rendered and interacted with a surface: - Open the **Stream** drawer to inspect accepted protocol lines, the selected - surface config, validation summaries, and validation retry feedback. + broker intent, selected surface config, validation summaries, and validation + retry feedback. - Open the **Devtools** drawer to inspect sandbox startup, render events, host tool requests, host dispatch, pushed state, trusted component sync, and stream diagnostics. @@ -138,17 +149,18 @@ rendered and interacted with a surface: ## Optional Checks -Open `http://localhost:5173/batch.html` to run several prompts through the same -surface config and allowed host tool set. Use it when changing prompt contracts, -directions, host tool wiring, visual direction coverage, or throughput behavior. +Open `http://localhost:5173/batch` to run several prompts through the +agent broker against the same host tool ceiling. Use it when changing prompt +contracts, directions, host tool wiring, visual direction coverage, or +throughput behavior. -Use the other `/generate.html` scenarios to exercise static summaries, +Use the other `/generate` scenarios to exercise static summaries, declarative forms, host AI calls, GitHub lookup, trusted host components, -background host work, approval-required publish, scripted interactive mode, +background host work, approval-required publish, local state and motion, token overrides, layout constraints, sibling summon, Ghost steering when configured, and validation retry diagnostics. -Open `http://localhost:5173/strict.html` to see the trusted host overlay pattern +Open `http://localhost:5173/strict` to see the trusted host overlay pattern for sensitive input. The generated sandbox describes the slot; the host owns the real input and pushes only safe state back. diff --git a/docs/adoption/security.md b/docs/adoption/security.md index 6944e53..cf1c5e3 100644 --- a/docs/adoption/security.md +++ b/docs/adoption/security.md @@ -39,14 +39,14 @@ for this choice is `SurfacePolicy.tier`. | --- | --- | --- | | Read-only | `SurfacePolicy.tier: "static"` | Summaries, cards, explainers, comparisons, and dashboards. Scripts and host tools are omitted. | | Declarative interactive | `SurfacePolicy.tier: "declarative"` | Production default for forms, search, pickers, loading/error/data states, foreach lists, and safe attribute binding. Uses only `data-summon-*`. | -| Scripted interactive | `SurfacePolicy.tier: "scripted"` | Restricted pilots that need custom keyboard handling, DOM-local state, or computed presentation that declarative bindings cannot express. | | Background host work | `SurfacePolicy.tier: "worker"` | Host-owned background work through worker-backed resources/actions. | | Requires approval | `SurfacePolicy.tier: "approval"` | Operations that require a host approval adapter before the handler runs. | Declarative interactive surfaces still support clicks, submits, mount-triggered reads, data resources, loading/error/data bindings, foreach -templates, text binding, and safe image/data attributes. They forbid generated -`', { mode: 'interactive' }); + assert.ok(codes(svgScript.issues).includes('script-not-granted')); +}); + +test('compiler blocks CSS imports and escaped external url values', () => { + const result = compileArtifactHtml( + '', + baseContext, + ); + + assert.deepEqual(codes(result.issues), ['css-import', 'external-url']); +}); + +test('compiler accepts declarative local state and motion primitives', () => { + const result = compileArtifactHtml( + '
', + { + mode: 'interactive', + scriptPolicy: 'forbid', + }, + ); + + assert.deepEqual(result.issues, []); +}); diff --git a/packages/engine/test/runtime-validator-fixtures.ts b/packages/engine/test/runtime-validator-fixtures.ts index d3876d4..5ffcdb4 100644 --- a/packages/engine/test/runtime-validator-fixtures.ts +++ b/packages/engine/test/runtime-validator-fixtures.ts @@ -7,14 +7,6 @@ export const baseContext = { definedTokens: new Set(['color-text', 'space-2', 'radius-pill']), }; -export const scriptedSurfacePlan = { - purpose: 'explore', - runtime: 'scripted', - data: 'embedded', - authority: 'host-action', - persistence: 'replayable', -} as const; - export function codes(issues: ContractIssue[]): string[] { return issues.map((issue) => issue.code).sort(); } diff --git a/packages/engine/test/runtime-validator-protocol.test.ts b/packages/engine/test/runtime-validator-protocol.test.ts index e79fd29..57f7a37 100644 --- a/packages/engine/test/runtime-validator-protocol.test.ts +++ b/packages/engine/test/runtime-validator-protocol.test.ts @@ -35,6 +35,30 @@ test('blocks malformed screen declarations', () => { assert.deepEqual(codes(issues), ['duplicate-section-id']); }); +test('blocks malformed block declarations and paths', () => { + assert.deepEqual( + codes(validateProtocolLine( + { op: 'set', path: '/section/hero', value: { blocks: ['headline', 'headline'] } }, + baseContext, + )), + ['duplicate-block-id'], + ); + assert.deepEqual( + codes(validateProtocolLine( + { op: 'set', path: '/section/hero', value: { blocks: [] } }, + baseContext, + )), + ['invalid-block-count'], + ); + assert.deepEqual( + codes(validateProtocolLine( + { op: 'add', path: '/section/hero/block/Bad_Block', html: '

Hi

' }, + baseContext, + )), + ['invalid-block-path'], + ); +}); + test('blocks generated host-owned surface meta paths', () => { assert.deepEqual( codes(validateProtocolLine( diff --git a/packages/engine/test/runtime-validator-safety.test.ts b/packages/engine/test/runtime-validator-safety.test.ts index bba82bb..e614180 100644 --- a/packages/engine/test/runtime-validator-safety.test.ts +++ b/packages/engine/test/runtime-validator-safety.test.ts @@ -4,7 +4,6 @@ import { validateHtmlFragment } from '../src/index.ts'; import { baseContext, codes, - scriptedSurfacePlan, } from './runtime-validator-fixtures.ts'; test('blocks static scripts and inline handlers', () => { @@ -38,17 +37,23 @@ test('blocks interactive scripts by default without a scripted surface plan', () assert.deepEqual(codes(issues), ['script-not-granted']); }); -test('accepts scripts only for scripted surface plan with allow policy', () => { +test('rejects legacy scripted surface plan with allow policy', () => { const issues = validateHtmlFragment( '', { mode: 'interactive', scriptPolicy: 'allow', - surfacePlan: scriptedSurfacePlan, + surfacePlan: { + purpose: 'explore', + runtime: 'scripted', + data: 'embedded', + authority: 'host-action', + persistence: 'replayable', + } as never, capabilities: [{ name: 'choose', kind: 'action', triggers: ['click'] }], }, ); - assert.deepEqual(issues, []); + assert.deepEqual(codes(issues), ['script-not-granted', 'surface-script-policy-removed']); }); test('blocks external assets and unsafe tags', () => { @@ -56,7 +61,13 @@ test('blocks external assets and unsafe tags', () => { '
', baseContext, ); - assert.deepEqual(codes(issues), ['external-url', 'external-url', 'unsafe-tag']); + assert.deepEqual(codes(issues), [ + 'external-url', + 'external-url', + 'unsafe-attribute', + 'unsafe-tag', + 'unsafe-tag', + ]); }); test('blocks unknown declarative and script intents', () => { @@ -66,21 +77,38 @@ test('blocks unknown declarative and script intents', () => { ...baseContext, mode: 'interactive', scriptPolicy: 'allow', - surfacePlan: scriptedSurfacePlan, + surfacePlan: { + purpose: 'explore', + runtime: 'scripted', + data: 'embedded', + authority: 'host-action', + persistence: 'replayable', + } as never, }, ); - assert.deepEqual(codes(issues), ['unknown-intent', 'unknown-intent']); + assert.deepEqual(codes(issues), [ + 'script-not-granted', + 'surface-script-policy-removed', + 'unknown-intent', + 'unknown-intent', + ]); }); -test('validates sandbox.emit by intent name only', () => { +test('rejects sandbox.emit even when the intent name is granted', () => { const issues = validateHtmlFragment( '', { mode: 'interactive', scriptPolicy: 'allow', - surfacePlan: scriptedSurfacePlan, + surfacePlan: { + purpose: 'explore', + runtime: 'scripted', + data: 'embedded', + authority: 'host-action', + persistence: 'replayable', + } as never, capabilities: [{ name: 'search', triggers: ['mount'] }], }, ); - assert.deepEqual(issues, []); + assert.deepEqual(codes(issues), ['script-not-granted', 'surface-script-policy-removed']); }); diff --git a/packages/engine/test/runtime-validator-surface-style.test.ts b/packages/engine/test/runtime-validator-surface-style.test.ts index c0f3789..7029a0f 100644 --- a/packages/engine/test/runtime-validator-surface-style.test.ts +++ b/packages/engine/test/runtime-validator-surface-style.test.ts @@ -8,7 +8,7 @@ test('surface plan blocks scripts and capabilities that exceed declarative stati '', { mode: 'interactive', - scriptPolicy: 'allow', + scriptPolicy: 'forbid', surfacePlan: { purpose: 'inform', runtime: 'declarative', diff --git a/packages/engine/test/section-accumulator.test.ts b/packages/engine/test/section-accumulator.test.ts index ea06670..96123e8 100644 --- a/packages/engine/test/section-accumulator.test.ts +++ b/packages/engine/test/section-accumulator.test.ts @@ -66,3 +66,173 @@ test('repeated section add replaces existing html without changing order', () => assert.match(acc.compose(), /Final answer/); assert.deepEqual(acc.snapshot().sections.map((section) => section.id), ['hero']); }); + +test('block fragments compose inside stable section wrappers', () => { + const acc = new SectionAccumulator(); + acc.applyDetailed({ op: 'set', path: '/screen', value: { sections: ['summary'] } }); + assert.deepEqual( + acc.applyDetailed({ op: 'set', path: '/section/summary', value: { blocks: ['headline', 'metrics'] } }), + { changed: true, kind: 'section', sectionId: 'summary', orderChanged: true }, + ); + acc.applyDetailed({ + op: 'add', + path: '/section/summary/block/headline', + html: '

Closeout

', + }); + acc.applyDetailed({ + op: 'add', + path: '/section/summary/block/metrics', + html: '

42 orders

', + }); + + assert.equal(acc.compose(), [ + '
', + '
', + '

Closeout

', + '
', + '
', + '

42 orders

', + '
', + '
', + ].join('\n')); +}); + +test('block replacement updates one block and whole-section add clears block state', () => { + const acc = new SectionAccumulator(); + acc.applyDetailed({ op: 'set', path: '/screen', value: { sections: ['summary'] } }); + acc.applyDetailed({ op: 'set', path: '/section/summary', value: { blocks: ['a', 'b'] } }); + acc.applyDetailed({ op: 'add', path: '/section/summary/block/a', html: '

A

' }); + acc.applyDetailed({ op: 'add', path: '/section/summary/block/b', html: '

Draft

' }); + + const replacement = acc.applyDetailed({ + op: 'add', + path: '/section/summary/block/b', + html: '

Final

', + }); + assert.equal(replacement.changed, true); + assert.equal(replacement.blockId, 'b'); + assert.match(acc.compose(), /

A<\/p>/); + assert.match(acc.compose(), /

Final<\/p>/); + assert.doesNotMatch(acc.compose(), /Draft/); + + acc.applyDetailed({ op: 'add', path: '/section/summary', html: '

Opaque
' }); + assert.equal( + acc.compose(), + '
\n
Opaque
\n
', + ); +}); + +test('html node patches compose into nested section HTML', () => { + const acc = new SectionAccumulator(); + acc.applyDetailed({ op: 'set', path: '/screen', value: { sections: ['main'] } }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/root', + html: '
', + }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/headline', + parent: 'root', + html: '

Closeout

', + }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/metric', + parent: 'root', + html: '
42
', + }); + + assert.equal(acc.compose(), [ + '
', + '
', + '

Closeout

', + '
42
', + '
', + '
', + ].join('\n')); +}); + +test('html node patches compose children into explicit node slots', () => { + const acc = new SectionAccumulator(); + acc.applyDetailed({ op: 'set', path: '/screen', value: { sections: ['main'] } }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/root', + html: '
', + }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/card', + parent: 'root', + html: '

Sales

Loading
', + }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/card-value', + parent: 'card', + html: '

$1,240

', + }); + + assert.equal(acc.compose(), [ + '
', + '
', + '

Sales

', + '

$1,240

', + '
', + '
', + '
', + ].join('\n')); + assert.doesNotMatch(acc.compose(), /data-summon-skeleton/); +}); + +test('html node replacement updates only that node in composed HTML', () => { + const acc = new SectionAccumulator(); + acc.applyDetailed({ op: 'set', path: '/screen', value: { sections: ['main'] } }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/root', + html: '
', + }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/a', + parent: 'root', + html: '

A

', + }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/b', + parent: 'root', + html: '

Draft

', + }); + + const replacement = acc.applyDetailed({ + op: 'add', + path: '/section/main/node/b', + parent: 'root', + html: '

Final

', + }); + assert.equal(replacement.changed, true); + assert.equal(replacement.nodeId, 'b'); + assert.equal(replacement.nodePatch?.parentId, 'root'); + assert.match(acc.compose(), /data-summon-node="a">A/); + assert.match(acc.compose(), /data-summon-node="b">Final/); + assert.doesNotMatch(acc.compose(), /Draft/); +}); + +test('whole-section add clears html node state', () => { + const acc = new SectionAccumulator(); + acc.applyDetailed({ op: 'set', path: '/screen', value: { sections: ['main'] } }); + acc.applyDetailed({ + op: 'add', + path: '/section/main/node/root', + html: '
', + }); + + acc.applyDetailed({ op: 'add', path: '/section/main', html: '
Opaque
' }); + assert.equal( + acc.compose(), + '
\n
Opaque
\n
', + ); +}); diff --git a/packages/engine/test/stream-graph.test.ts b/packages/engine/test/stream-graph.test.ts index c3ad7f3..ca4f442 100644 --- a/packages/engine/test/stream-graph.test.ts +++ b/packages/engine/test/stream-graph.test.ts @@ -50,6 +50,46 @@ test('section adds mark nodes present and increment revisions on replacement', ( assert.deepEqual(graph.snapshot().health.missingDeclared, []); }); +test('block fragments add optional diagnostics under section nodes', () => { + const graph = new StreamGraph(); + graph.applyLine({ op: 'set', path: '/screen', value: { sections: ['summary'] } }); + graph.applyLine({ op: 'set', path: '/section/summary', value: { blocks: ['headline', 'metrics'] } }); + graph.applyLine({ op: 'add', path: '/section/summary/block/headline', html: '

Ready

' }); + + const summary = graph.snapshot().sections[0]!; + assert.equal(summary.present, true); + assert.equal(summary.revision, 1); + assert.equal(summary.declaredBlockCount, 2); + assert.equal(summary.presentBlockCount, 1); + assert.deepEqual( + summary.blocks?.map(({ id, declared, present, revision }) => ({ id, declared, present, revision })), + [ + { id: 'headline', declared: true, present: true, revision: 1 }, + { id: 'metrics', declared: true, present: false, revision: 0 }, + ], + ); +}); + +test('block path issues attach to parent section and block diagnostics', () => { + const graph = new StreamGraph(); + const issue: ContractIssue = { + source: 'protocol', + severity: 'warn', + code: 'undeclared-block', + message: 'Block was not declared', + path: '/section/summary/block/metrics', + }; + + graph.recordIssue(issue); + + const summary = graph.snapshot().sections[0]!; + assert.equal(summary.id, 'summary'); + assert.equal(summary.lastIssue?.code, 'undeclared-block'); + assert.equal(summary.lastBlockIssue?.code, 'undeclared-block'); + assert.equal(summary.blocks?.[0]?.id, 'metrics'); + assert.equal(summary.blocks?.[0]?.lastIssue?.code, 'undeclared-block'); +}); + test('add-before-screen records undeclared present state', () => { const graph = new StreamGraph(); graph.applyLine({ op: 'add', path: '/section/hero', html: '

Hero

' }); diff --git a/packages/engine/test/surface-policy.test.ts b/packages/engine/test/surface-policy.test.ts index 6d80f0c..2fbee6d 100644 --- a/packages/engine/test/surface-policy.test.ts +++ b/packages/engine/test/surface-policy.test.ts @@ -128,15 +128,15 @@ test('compiles declarative policy and narrows grants, components, and patterns', }); }); -test('compiles scripted policy with scripts enabled', () => { +test('rejects removed scripted policy tier', () => { const compiled = compileSurfacePolicy({ tier: 'scripted', grants: ['choose'], - }, { capabilities }); - assert.deepEqual(compiled.issues, []); - assert.equal(compiled.mode, 'interactive'); - assert.equal(compiled.scriptPolicy, 'allow'); - assert.equal(compiled.surfacePlan.runtime, 'scripted'); + } as never, { capabilities }); + assert.deepEqual(compiled.issues.map((issue) => issue.code), ['surface-policy-invalid']); + assert.equal(compiled.mode, 'static'); + assert.equal(compiled.scriptPolicy, 'forbid'); + assert.equal(compiled.surfacePlan.runtime, 'static'); }); test('compiles worker policy and requires worker-backed surface area', () => { diff --git a/packages/host/src/browser.ts b/packages/host/src/browser.ts index 7e842fa..048dce5 100644 --- a/packages/host/src/browser.ts +++ b/packages/host/src/browser.ts @@ -41,6 +41,8 @@ export type { } from './component-islands.js'; export type { Artifact, + CompiledArtifactHtml, + CompiledHtmlNodePatch, ComponentIslandBounds, ComponentIslandDescriptor, ComponentsMessage, diff --git a/packages/host/src/index.ts b/packages/host/src/index.ts index 687223f..e860c30 100644 --- a/packages/host/src/index.ts +++ b/packages/host/src/index.ts @@ -89,6 +89,8 @@ export type { } from './strict-input.js'; export type { Artifact, + CompiledArtifactHtml, + CompiledHtmlNodePatch, ComponentIslandBounds, ComponentIslandDescriptor, ComponentsMessage, diff --git a/packages/host/src/sandbox-spawner.ts b/packages/host/src/sandbox-spawner.ts index 77e190a..7a30418 100644 --- a/packages/host/src/sandbox-spawner.ts +++ b/packages/host/src/sandbox-spawner.ts @@ -2,33 +2,35 @@ import type { EventStore } from '@summon-internal/devtools'; import { hasCompleteResourceStateKeys, type ValidationCapability } from '@summon-internal/engine'; import type { Artifact, + CompiledHtmlNodePatch, ComponentIslandDescriptor, SandboxHandle, SandboxInboundMessage, } from './types.js'; /** - * CSP applied inside every Summon sandbox. `'unsafe-inline'` for scripts is safe here - * because (a) the iframe is null-origin via sandbox="allow-scripts", so there is no - * trusted origin for a script to abuse, and (b) `connect-src 'none'` prevents any - * outbound network. What runs inline has nowhere to exfiltrate to and no parent - * DOM to touch. + * CSP applied inside every Summon sandbox. Scripts are nonce-authorized trusted + * bootstrap/resource scripts only; generated artifact HTML arrives after + * SUMMON_READY and never receives a script nonce. Generated CSS remains inline + * because the compiler constrains it and visual richness is a core Summon goal. */ -const CSP = [ - "default-src 'none'", - "script-src 'unsafe-inline'", - "style-src 'unsafe-inline'", - "img-src data:", - "font-src data:", - "connect-src 'none'", - "form-action 'none'", - "base-uri 'none'", - "frame-src 'none'", - "child-src 'none'", - "media-src 'none'", - "object-src 'none'", - "worker-src 'none'", -].join('; '); +function cspForNonce(nonce: string): string { + return [ + "default-src 'none'", + `script-src 'nonce-${nonce}'`, + "style-src 'unsafe-inline'", + "img-src data:", + "font-src data:", + "connect-src 'none'", + "form-action 'none'", + "base-uri 'none'", + "frame-src 'none'", + "child-src 'none'", + "media-src 'none'", + "object-src 'none'", + "worker-src 'none'", + ].join('; '); +} export interface SpawnOptions { iframe: HTMLIFrameElement; @@ -87,17 +89,21 @@ function escapeHtml(s: string): string { .replaceAll('"', '"'); } +function escapeScriptJson(value: unknown): string { + return JSON.stringify(value).replaceAll('<', '\\u003c'); +} + function buildSrcdoc(params: { sandboxId: string; + scriptNonce: string; bootstrapSource: string; tokensSource: string; - bodyHtml: string; resourceMap: ResourceMap; }): string { // The CSP meta must come FIRST in — anything before it is unprotected. - // Artifact HTML always renders inside #summon-root so the bootstrap can swap - // content post-spawn via SUMMON_RENDER messages without touching bootstrap or - // tokens. + // Artifact HTML is deliberately absent from initial srcdoc. The trusted + // bootstrap sends SUMMON_READY, then the host queues the compiled render + // through SUMMON_RENDER. // // The base style block (entrance animation for live-paint sections) is // emitted BEFORE the direction's tokensSource so directions can override — @@ -106,16 +112,17 @@ function buildSrcdoc(params: { return ` - + - - - + + + -
${params.bodyHtml}
+
+ `; } @@ -124,12 +131,141 @@ const SUMMON_BASE_CSS = ` [data-summon-section] { animation: summon-section-in 0.45s cubic-bezier(0.33, 1, 0.68, 1) both; } +.summon-node-enter { + animation: summon-node-enter 0.32s cubic-bezier(0.33, 1, 0.68, 1) both; + will-change: opacity, filter, transform; +} +.summon-node-update { + animation: summon-node-update 0.42s ease-out both; +} +.summon-slot-filled { + animation: summon-slot-filled 0.42s ease-out both; +} +.summon-motion-enter-rise { + animation: summon-motion-rise 0.34s cubic-bezier(0.33, 1, 0.68, 1) both; +} +.summon-motion-enter-fade { + animation: summon-motion-fade 0.24s ease-out both; +} +.summon-motion-enter-fade-slide { + animation: summon-motion-fade-slide 0.32s cubic-bezier(0.33, 1, 0.68, 1) both; +} +.summon-motion-enter-pop { + animation: summon-motion-pop 0.28s cubic-bezier(0.2, 0.9, 0.2, 1.2) both; +} +.summon-motion-update-pulse { + animation: summon-motion-pulse 0.46s ease-out both; +} +.summon-motion-update-fade { + animation: summon-motion-update-fade 0.28s ease-out both; +} +.summon-motion-update-pop { + animation: summon-motion-pop 0.24s cubic-bezier(0.2, 0.9, 0.2, 1.2) both; +} +.summon-transition-fade, +.summon-transition-rise, +.summon-transition-fade-slide, +.summon-transition-pop { + transition: + opacity 0.18s ease-out, + filter 0.18s ease-out, + transform 0.18s ease-out, + box-shadow 0.18s ease-out; +} +[data-summon-skeleton] { + position: relative; + overflow: hidden; + min-height: 0.8em; + border-radius: 6px; + color: transparent !important; + background: rgba(127, 127, 127, 0.14); + pointer-events: none; + user-select: none; +} +[data-summon-skeleton] > * { + visibility: hidden; +} +[data-summon-skeleton]::after { + content: ""; + position: absolute; + inset: 0; + transform: translateX(-100%); + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.38), transparent); + animation: summon-skeleton-sheen 1.35s ease-in-out infinite; +} @keyframes summon-section-in { from { opacity: 0; filter: blur(8px); transform: translateY(8px); } to { opacity: 1; filter: blur(0); transform: translateY(0); } } +@keyframes summon-node-enter { + from { opacity: 0; filter: blur(5px); transform: translateY(6px); } + to { opacity: 1; filter: blur(0); transform: translateY(0); } +} +@keyframes summon-node-update { + 0% { box-shadow: 0 0 0 0 rgba(80, 112, 255, 0); } + 35% { box-shadow: 0 0 0 2px rgba(80, 112, 255, 0.18); } + 100% { box-shadow: 0 0 0 0 rgba(80, 112, 255, 0); } +} +@keyframes summon-slot-filled { + 0% { box-shadow: inset 0 0 0 0 rgba(80, 112, 255, 0); } + 45% { box-shadow: inset 0 0 0 1px rgba(80, 112, 255, 0.12); } + 100% { box-shadow: inset 0 0 0 0 rgba(80, 112, 255, 0); } +} +@keyframes summon-motion-rise { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes summon-motion-fade { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes summon-motion-fade-slide { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes summon-motion-pop { + from { opacity: 0; transform: scale(0.985); } + to { opacity: 1; transform: scale(1); } +} +@keyframes summon-motion-pulse { + 0% { box-shadow: 0 0 0 0 rgba(80, 112, 255, 0); } + 35% { box-shadow: 0 0 0 3px rgba(80, 112, 255, 0.16); } + 100% { box-shadow: 0 0 0 0 rgba(80, 112, 255, 0); } +} +@keyframes summon-motion-update-fade { + 0% { opacity: 0.72; } + 100% { opacity: 1; } +} +@keyframes summon-skeleton-sheen { + 0% { transform: translateX(-100%); } + 60%, 100% { transform: translateX(100%); } +} @media (prefers-reduced-motion: reduce) { - [data-summon-section] { animation: none; } + [data-summon-section], + .summon-node-enter, + .summon-node-update, + .summon-slot-filled, + .summon-motion-enter-rise, + .summon-motion-enter-fade, + .summon-motion-enter-fade-slide, + .summon-motion-enter-pop, + .summon-motion-update-pulse, + .summon-motion-update-fade, + .summon-motion-update-pop, + [data-summon-skeleton]::after { + animation: none; + } + .summon-transition-fade, + .summon-transition-rise, + .summon-transition-fade-slide, + .summon-transition-pop { + transition: none; + } + .summon-node-enter { + opacity: 1; + filter: none; + transform: none; + } } `; @@ -204,6 +340,7 @@ function normalizeComponentDescriptors(raw: unknown): ComponentIslandDescriptor[ export function spawnSandbox(opts: SpawnOptions): SandboxHandle { const sandboxId = randomSandboxId(); + const scriptNonce = randomSandboxId(); // Bridge allowlist comes only from the host grant. A JS caller that omits // grantedIntents fails closed because `new Set(undefined)` grants nothing. const intentAllowlist = new Set(opts.grantedIntents); @@ -216,7 +353,10 @@ export function spawnSandbox(opts: SpawnOptions): SandboxHandle { let ready = false; const pendingStates: Record[] = []; - const pendingRenders: string[] = []; + const pendingDomOps: Array< + | { kind: 'render'; html: string } + | { kind: 'node-patch'; patch: CompiledHtmlNodePatch } + > = []; // Chrome attributes are merged before flush so a flurry of setChrome calls // pre-ready collapses into a single postMessage. Post-ready, each setChrome // call dispatches immediately. @@ -235,11 +375,15 @@ export function spawnSandbox(opts: SpawnOptions): SandboxHandle { } while (pendingStates.length > 0) { const state = pendingStates.shift()!; - opts.iframe.contentWindow.postMessage({ type: 'SUMMON_STATE', state }, '*'); + opts.iframe.contentWindow.postMessage({ type: 'SUMMON_STATE', sandbox_id: sandboxId, state }, '*'); } - while (pendingRenders.length > 0) { - const html = pendingRenders.shift()!; - opts.iframe.contentWindow.postMessage({ type: 'SUMMON_RENDER', html }, '*'); + while (pendingDomOps.length > 0) { + const op = pendingDomOps.shift()!; + if (op.kind === 'render') { + opts.iframe.contentWindow.postMessage({ type: 'SUMMON_RENDER', sandbox_id: sandboxId, html: op.html }, '*'); + } else { + opts.iframe.contentWindow.postMessage({ type: 'SUMMON_NODE_PATCH', sandbox_id: sandboxId, patch: op.patch }, '*'); + } } } @@ -361,11 +505,14 @@ export function spawnSandbox(opts: SpawnOptions): SandboxHandle { opts.iframe.srcdoc = buildSrcdoc({ sandboxId, + scriptNonce, bootstrapSource: opts.bootstrapSource, tokensSource: opts.tokensSource, - bodyHtml: opts.artifact.html, resourceMap, }); + if (opts.artifact.html) { + pendingDomOps.push({ kind: 'render', html: opts.artifact.html }); + } opts.events?.push({ kind: 'sandbox-spawned', @@ -384,7 +531,7 @@ export function spawnSandbox(opts: SpawnOptions): SandboxHandle { flushPending(); }, render(html) { - pendingRenders.push(html); + pendingDomOps.push({ kind: 'render', html }); opts.events?.push({ kind: 'render', at: Date.now(), @@ -393,6 +540,16 @@ export function spawnSandbox(opts: SpawnOptions): SandboxHandle { }); flushPending(); }, + patchNode(patch) { + pendingDomOps.push({ kind: 'node-patch', patch }); + opts.events?.push({ + kind: 'render', + at: Date.now(), + sandboxId, + bytes: patch.html.length, + }); + flushPending(); + }, setChrome(attrs) { // Validate keys defensively. We get away with `unsafe-inline` everywhere // else because the iframe is null-origin, but `data-summon-` is diff --git a/packages/host/src/surface-envelope.ts b/packages/host/src/surface-envelope.ts index 5a959b1..6c8e3e6 100644 --- a/packages/host/src/surface-envelope.ts +++ b/packages/host/src/surface-envelope.ts @@ -1,4 +1,5 @@ import type { + CompiledArtifactHtml, ContractIssue, ProtocolLine, StreamGraphSnapshot, @@ -7,23 +8,25 @@ import type { ValidationComponent, } from '@summon-internal/engine'; import { + compileArtifactHtml, isProtocolLine, normalizeSurfacePlan, surfacePlanScriptPolicy, - validateHtmlFragment, validateProtocolLine, } from '@summon-internal/engine'; -export const SUMMON_SURFACE_ENVELOPE_VERSION = 1; +export const SUMMON_SURFACE_ENVELOPE_VERSION = 2; export interface SurfaceEnvelope { - version: 1; + version: 2; id: string; createdAt: string; prompt: string; surfacePlan: SurfacePlan; protocolLines: ProtocolLine[]; - html: string; + compiledHtml: CompiledArtifactHtml; + compilerVersion: string; + compilerIssues: ContractIssue[]; validationIssues: ContractIssue[]; streamGraph: StreamGraphSnapshot | null; grants: { @@ -64,15 +67,23 @@ export function createSurfaceEnvelope(input: CreateSurfaceEnvelopeInput): Surfac const createdAt = input.createdAt instanceof Date ? input.createdAt.toISOString() : input.createdAt ?? new Date().toISOString(); + const validationContext = validationContextForEnvelope(input); + const compiled = compileArtifactHtml(input.html, validationContext); + const protocolIssues: ContractIssue[] = []; + const protocolLines = input.protocolLines.map((line) => + compileEnvelopeProtocolLine(line, validationContext, protocolIssues), + ); return { - version: 1, + version: 2, id: input.id ?? newEnvelopeId(), createdAt, prompt: input.prompt, surfacePlan: input.surfacePlan, - protocolLines: input.protocolLines.map((line) => ({ ...line })), - html: input.html, - validationIssues: input.validationIssues ?? [], + protocolLines, + compiledHtml: compiled.html, + compilerVersion: compiled.compilerVersion, + compilerIssues: compiled.issues, + validationIssues: [...(input.validationIssues ?? []), ...compiled.issues, ...protocolIssues], streamGraph: input.streamGraph ?? null, grants: { intents: [...input.grants.intents], @@ -81,7 +92,7 @@ export function createSurfaceEnvelope(input: CreateSurfaceEnvelopeInput): Surfac }, metadata: input.metadata ?? {}, tokenCss: input.tokenCss ?? null, - runtimeVersion: input.runtimeVersion ?? 'summon-surface-envelope-v1', + runtimeVersion: input.runtimeVersion ?? 'summon-surface-envelope-v2', }; } @@ -94,8 +105,15 @@ export function parseSurfaceEnvelope(raw: string | unknown): SurfaceEnvelope | n return null; } } - if (!isSurfaceEnvelope(parsed)) return null; - return createSurfaceEnvelope(parsed); + if (isSurfaceEnvelope(parsed)) { + return parsed; + } + if (isLegacySurfaceEnvelope(parsed)) { + const envelope = createSurfaceEnvelope(parsed); + if (envelope.compilerIssues.some((issue) => issue.severity === 'block')) return null; + return envelope; + } + return null; } export function isSurfaceEnvelope(value: unknown): value is SurfaceEnvelope { @@ -105,7 +123,11 @@ export function isSurfaceEnvelope(value: unknown): value is SurfaceEnvelope { if (typeof input.id !== 'string' || !input.id) return false; if (typeof input.createdAt !== 'string' || Number.isNaN(Date.parse(input.createdAt))) return false; if (typeof input.prompt !== 'string') return false; - if (typeof input.html !== 'string') return false; + if (typeof input.compiledHtml !== 'string') return false; + if (typeof input.compilerVersion !== 'string' || !input.compilerVersion) return false; + if (!Array.isArray(input.compilerIssues) || !input.compilerIssues.every(isContractIssue)) { + return false; + } if (typeof input.runtimeVersion !== 'string' || !input.runtimeVersion) return false; const surfacePlan = normalizeSurfacePlan(input.surfacePlan); @@ -124,23 +146,62 @@ export function isSurfaceEnvelope(value: unknown): value is SurfaceEnvelope { return false; } - const grants = input.grants as SurfaceEnvelope['grants']; - const metadata = input.metadata as SurfaceEnvelope['metadata']; - const validationContext = { - mode: metadata.mode ?? (surfacePlan.runtime === 'static' ? 'static' as const : 'interactive' as const), - scriptPolicy: surfacePlanScriptPolicy(surfacePlan), - capabilities: grants.capabilities, - components: grants.components, - allowedIntents: grants.intents, - surfacePlan, - }; + const validationContext = validationContextForEnvelope(input as unknown as CreateSurfaceEnvelopeInput); for (const line of input.protocolLines as ProtocolLine[]) { const issues = validateProtocolLine(line, validationContext); if (issues.some((issue) => issue.severity === 'block')) return false; } - const htmlIssues = validateHtmlFragment(input.html, validationContext); - if (htmlIssues.some((issue) => issue.severity === 'block')) return false; + const compiled = compileArtifactHtml(input.compiledHtml, validationContext); + if (compiled.issues.some((issue) => issue.severity === 'block')) return false; + if (compiled.html !== input.compiledHtml) return false; + + return true; +} + +function compileEnvelopeProtocolLine( + line: ProtocolLine, + validationContext: ReturnType, + issues: ContractIssue[], +): ProtocolLine { + if (line.op !== 'add' || line.html === undefined) return { ...line }; + const compiled = compileArtifactHtml(line.html, validationContext); + for (const issue of compiled.issues) { + issues.push(issue.path ? issue : { ...issue, path: line.path }); + } + return { ...line, html: compiled.html }; +} + +function validationContextForEnvelope(input: { + surfacePlan: SurfacePlan; + grants: SurfaceEnvelope['grants']; + metadata?: SurfaceEnvelope['metadata']; +}) { + return { + mode: input.metadata?.mode ?? (input.surfacePlan.runtime === 'static' ? 'static' as const : 'interactive' as const), + scriptPolicy: surfacePlanScriptPolicy(input.surfacePlan), + capabilities: input.grants.capabilities, + components: input.grants.components, + allowedIntents: input.grants.intents, + surfacePlan: input.surfacePlan, + }; +} +function isLegacySurfaceEnvelope(value: unknown): value is CreateSurfaceEnvelopeInput { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const input = value as Record; + if (input.version !== 1) return false; + if (typeof input.id !== 'string' || !input.id) return false; + if (typeof input.createdAt !== 'string' || Number.isNaN(Date.parse(input.createdAt))) return false; + if (typeof input.prompt !== 'string') return false; + if (typeof input.html !== 'string') return false; + const surfacePlan = normalizeSurfacePlan(input.surfacePlan); + if (!surfacePlan) return false; + if (!Array.isArray(input.protocolLines) || !input.protocolLines.every(isProtocolLine)) return false; + if (!Array.isArray(input.validationIssues) || !input.validationIssues.every(isContractIssue)) return false; + if (!isStreamGraphSnapshot(input.streamGraph)) return false; + if (!isGrants(input.grants)) return false; + if (!isMetadata(input.metadata)) return false; + if (input.tokenCss !== undefined && input.tokenCss !== null && typeof input.tokenCss !== 'string') return false; return true; } diff --git a/packages/host/src/surface-stream.ts b/packages/host/src/surface-stream.ts index dd1d1e5..36d150b 100644 --- a/packages/host/src/surface-stream.ts +++ b/packages/host/src/surface-stream.ts @@ -1,13 +1,17 @@ import { + compileArtifactHtml, parseProtocolLine, SectionAccumulator, StreamGraph, + type CompiledArtifactHtml, + type CompiledHtmlNodePatch, type ContractIssue, type MetaLine, type ProtocolLine, type SectionApplyResult, type StreamGraphSnapshot, type SurfacePlanMode, + type ValidationContext, } from '@summon-internal/engine'; export type SurfaceStreamChunk = string | Uint8Array; @@ -64,18 +68,23 @@ export interface SurfaceStreamOptions { context: SurfaceStreamContext, ) => void | Promise; onRenderHtml?: ( - html: string, + html: CompiledArtifactHtml, + context: SurfaceStreamContext, + ) => void | Promise; + onNodePatch?: ( + patch: CompiledHtmlNodePatch, context: SurfaceStreamContext, ) => void | Promise; onError?: ( error: Error, context: SurfaceStreamContext, ) => void | Promise; + validationContext?: ValidationContext; } export interface SurfaceStreamResult { protocolLines: ProtocolLine[]; - html: string; + html: CompiledArtifactHtml; streamGraph: StreamGraphSnapshot; validationIssues: ContractIssue[]; parseErrors: SurfaceStreamParseError[]; @@ -122,7 +131,7 @@ export async function consumeSurfaceStream( const emitRender = async (ctx: SurfaceStreamContext) => { if (!accumulator.hasAnySection()) return; - await options.onRenderHtml?.(accumulator.compose(), ctx); + await options.onRenderHtml?.(accumulator.compose() as CompiledArtifactHtml, ctx); }; const handleRawLine = async (raw: string) => { @@ -147,32 +156,42 @@ export async function consumeSurfaceStream( return; } - graph.applyLine(line); + const acceptedLine = compileAcceptedLine(line, options.validationContext, validationIssues, graph); + if (!acceptedLine) { + await emitGraph(context(raw)); + return; + } + + graph.applyLine(acceptedLine); let applyResult: SectionApplyResult | undefined; - if (line.op !== 'meta') { - applyResult = accumulator.applyDetailed(line); + if (acceptedLine.op !== 'meta') { + applyResult = accumulator.applyDetailed(acceptedLine); acceptedStructuralLines += 1; } const ctx = context(raw, applyResult); - protocolLines.push(line); + protocolLines.push(acceptedLine); - if (line.op === 'meta' && line.path === '/validation-blocked' && isContractIssue(line.value)) { - pushValidationIssue(validationIssues, line.value); + if (acceptedLine.op === 'meta' && acceptedLine.path === '/validation-blocked' && isContractIssue(acceptedLine.value)) { + pushValidationIssue(validationIssues, acceptedLine.value); } - if (line.op === 'meta' && line.path === '/validation-summary') { - for (const issue of validationSummaryExamples(line.value)) { + if (acceptedLine.op === 'meta' && acceptedLine.path === '/validation-summary') { + for (const issue of validationSummaryExamples(acceptedLine.value)) { pushValidationIssue(validationIssues, issue); } } - await options.onLine?.(line, ctx); - if (line.op === 'meta') { - await options.onMeta?.(line, ctx); + await options.onLine?.(acceptedLine, ctx); + if (acceptedLine.op === 'meta') { + await options.onMeta?.(acceptedLine, ctx); return; } await emitGraph(ctx); if (renderMode() === 'live' && applyResult?.changed) { + if (applyResult.nodePatch && options.onNodePatch) { + await options.onNodePatch(applyResult.nodePatch as CompiledHtmlNodePatch, ctx); + return; + } await emitRender(ctx); } }; @@ -205,7 +224,7 @@ export async function consumeSurfaceStream( throw error; } - const html = accumulator.compose(); + const html = accumulator.compose() as CompiledArtifactHtml; return { protocolLines, html, @@ -217,6 +236,25 @@ export async function consumeSurfaceStream( }; } +function compileAcceptedLine( + line: ProtocolLine, + validationContext: ValidationContext | undefined, + validationIssues: ContractIssue[], + graph: StreamGraph, +): ProtocolLine | null { + if (!validationContext || line.op !== 'add' || line.html === undefined) return line; + const result = compileArtifactHtml(line.html, validationContext); + let blocked = false; + for (const issue of result.issues) { + const scoped = issue.path ? issue : { ...issue, path: line.path }; + pushValidationIssue(validationIssues, scoped); + graph.recordIssue(scoped); + if (issue.severity === 'block') blocked = true; + } + if (blocked) return null; + return { ...line, html: result.html }; +} + async function* chunksFromSource( source: SurfaceStreamSource, ): AsyncGenerator { diff --git a/packages/host/src/types.ts b/packages/host/src/types.ts index e5fda2c..3880828 100644 --- a/packages/host/src/types.ts +++ b/packages/host/src/types.ts @@ -1,11 +1,35 @@ -import type { ValidationCapability, ValidationComponent } from '@summon-internal/engine'; +import type { + CompiledArtifactHtml, + CompiledHtmlNodePatch, + ValidationCapability, + ValidationComponent, +} from '@summon-internal/engine'; + +export type { + CompiledArtifactHtml, + CompiledHtmlNodePatch, + HtmlNodePatch, +} from '@summon-internal/engine'; /** Messages from host into the sandbox iframe. */ export interface StateMessage { type: 'SUMMON_STATE'; + sandbox_id: string; state: Record; } +export interface NodePatchMessage { + type: 'SUMMON_NODE_PATCH'; + sandbox_id: string; + patch: CompiledHtmlNodePatch; +} + +export interface RenderMessage { + type: 'SUMMON_RENDER'; + sandbox_id: string; + html: CompiledArtifactHtml; +} + /** * Host → iframe declaration of chrome attributes the artifact's CSS may * target. Each entry becomes `="">` inside the @@ -78,8 +102,10 @@ export interface SandboxHandle { iframe: HTMLIFrameElement; /** Push new state into the sandbox. Replaces current state on the sandbox side. */ pushState(state: Record): void; - /** Replace the HTML inside #summon-root. Scripts in the new HTML will execute. */ - render(html: string): void; + /** Replace the compiled HTML inside #summon-root. */ + render(html: CompiledArtifactHtml): void; + /** Patch one validated data-summon-node subtree in place. Experimental. */ + patchNode(patch: CompiledHtmlNodePatch): void; /** * Declare chrome attributes that should appear on the sandbox document's * `` element. Each entry becomes `data-summon-=""` and is @@ -101,8 +127,8 @@ export interface Artifact { capabilities?: ValidationCapability[]; /** Advisory components the artifact claims to use. Host registry remains the rendering grant. */ components?: ValidationComponent[]; - /** Full HTML body to render inside the sandbox. */ - html: string; + /** Compiled canonical HTML body to render inside the sandbox. */ + html: CompiledArtifactHtml; /** Optional initial state pushed after SANDBOX_READY. */ initialState?: Record; } diff --git a/packages/host/test/capability-registry.test.ts b/packages/host/test/capability-registry.test.ts index bf6e784..b9b934b 100644 --- a/packages/host/test/capability-registry.test.ts +++ b/packages/host/test/capability-registry.test.ts @@ -909,8 +909,11 @@ test('surface envelope serializes replay metadata', () => { runtimeVersion: 'test', }); - assert.equal(envelope.version, 1); + assert.equal(envelope.version, 2); assert.equal(envelope.prompt, 'compare options'); + assert.equal(envelope.compiledHtml, '
Saved
'); + assert.equal(envelope.compilerIssues.length, 0); + assert.equal(envelope.compilerVersion, 'summon-artifact-compiler-v2'); assert.equal(envelope.metadata.directionId, 'ghost'); assert.deepEqual(envelope.protocolLines, [ { op: 'set', path: '/screen', value: { sections: ['hero'] } }, @@ -962,6 +965,6 @@ test('surface envelope parser rejects malformed, wrong-version, and escalating e }); assert.equal(parseSurfaceEnvelope('{bad'), null); - assert.equal(parseSurfaceEnvelope({ ...envelope, version: 2 }), null); + assert.equal(parseSurfaceEnvelope({ ...envelope, version: 3 }), null); assert.equal(parseSurfaceEnvelope(envelope), null); }); diff --git a/packages/host/test/surface-stream.test.ts b/packages/host/test/surface-stream.test.ts index ce097b8..2861963 100644 --- a/packages/host/test/surface-stream.test.ts +++ b/packages/host/test/surface-stream.test.ts @@ -177,6 +177,40 @@ test('consumeSurfaceStream live render mode renders interactive section replacem assert.equal(renders[1], result.html); }); +test('consumeSurfaceStream emits live html node patches when a hook is provided', async () => { + const renders: string[] = []; + const patches: string[] = []; + const result = await consumeSurfaceStream([ + '{"op":"set","path":"/screen","value":{"sections":["main"]}}\n', + '{"op":"add","path":"/section/main/node/root","html":"
"}\n', + '{"op":"add","path":"/section/main/node/headline","parent":"root","html":"

Ready

"}\n', + ], { + mode: 'static', + renderMode: 'live', + onRenderHtml: (html) => renders.push(html), + onNodePatch: (patch) => patches.push(`${patch.sectionId}/${patch.nodeId}/${patch.parentId ?? ''}`), + }); + + assert.deepEqual(renders, []); + assert.deepEqual(patches, ['main/root/', 'main/headline/root']); + assert.match(result.html, /data-summon-node="headline"/); +}); + +test('consumeSurfaceStream falls back to composed html for node patches without a hook', async () => { + const renders: string[] = []; + await consumeSurfaceStream([ + '{"op":"set","path":"/screen","value":{"sections":["main"]}}\n', + '{"op":"add","path":"/section/main/node/root","html":"
"}\n', + ], { + mode: 'static', + renderMode: 'live', + onRenderHtml: (html) => renders.push(html), + }); + + assert.equal(renders.length, 1); + assert.match(renders[0]!, /data-summon-node="root"/); +}); + test('consumeSurfaceStream manual render mode does not call render callback', async () => { let renderCount = 0; const result = await consumeSurfaceStream([ diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 0a6fcd6..682911f 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -3,7 +3,15 @@ import { type ComponentDefinition, type ComponentRegistry, } from '@anarchitecture/summon'; -import { SectionAccumulator, type ProtocolLine } from '@anarchitecture/summon/engine'; +import { + compileArtifactHtml, + SectionAccumulator, + type CompiledArtifactHtml, + type CompiledHtmlNodePatch, + type HtmlNodePatch, + type ProtocolLine, + type ValidationContext, +} from '@anarchitecture/summon/engine'; import { createComponentIslandRegistry, spawnSandbox, @@ -19,7 +27,16 @@ import { bootstrapSource as defaultBootstrapSource, tokensSource as defaultTokensSource, } from '@anarchitecture/summon/assets'; -import { createElement, useEffect, useMemo, useRef, type ComponentType, type CSSProperties } from 'react'; +import { + createElement, + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, + type ComponentType, + type CSSProperties, +} from 'react'; import { createRoot, type Root } from 'react-dom/client'; export interface SummonSurfaceChrome { @@ -30,25 +47,91 @@ export interface SummonSurfaceProps { envelope?: SurfaceEnvelope | null; html?: string; protocolLines?: ProtocolLine[]; + artifactIntents?: string[]; + grantedIntents?: string[]; + grantedCapabilities?: Artifact['capabilities']; + artifactComponents?: Artifact['components']; capabilityRegistry?: CapabilityRegistry | null; componentRegistry?: ComponentRegistry | null; bootstrapSource?: string; tokensSource?: string; initialState?: Record; chrome?: SummonSurfaceChrome; + onIntent?: (intent: string, args: Record) => void; + onIntentRejected?: (reason: string, raw: unknown) => void; onEvent?: (event: DevtoolsEvent) => void; onFatal?: (reason: string) => void; onHandlerError?: (intent: string, error: Error) => void; onComponentError?: (error: ComponentIslandError) => void; + id?: string; title?: string; className?: string; style?: CSSProperties; } -export function SummonSurface(props: SummonSurfaceProps) { +export interface SummonSurfaceHandle { + iframe: HTMLIFrameElement | null; + sandboxId: string | null; + render(html: string): void; + patchNode(patch: HtmlNodePatch): void; + pushState(state: Record): void; + setChrome(chrome: SummonSurfaceChrome): void; +} + +export const SummonSurface = forwardRef(function SummonSurface( + props, + ref, +) { const iframeRef = useRef(null); + const handleRef = useRef(null); + const validationContextRef = useRef(null); + const lastRenderedHtmlRef = useRef(null); const events = useMemo(() => createEventStore(), []); + useImperativeHandle(ref, () => ({ + get iframe() { + return iframeRef.current; + }, + get sandboxId() { + return handleRef.current?.sandboxId ?? null; + }, + render(html: string) { + const compiled = compileForRender(html, validationContextRef.current ?? defaultValidationContext()); + lastRenderedHtmlRef.current = compiled; + preflightComponentProps( + compiled, + props.componentRegistry, + handleRef.current?.sandboxId ?? undefined, + events, + props.onComponentError, + ); + handleRef.current?.render(compiled); + }, + patchNode(patch: HtmlNodePatch) { + const compiled = compilePatchForRender(patch, validationContextRef.current ?? defaultValidationContext()); + if (compiled) { + preflightComponentProps( + compiled.html, + props.componentRegistry, + handleRef.current?.sandboxId ?? undefined, + events, + props.onComponentError, + ); + handleRef.current?.patchNode(compiled); + } + }, + pushState(state: Record) { + handleRef.current?.pushState(state); + }, + setChrome(chrome: SummonSurfaceChrome) { + handleRef.current?.setChrome(chrome); + }, + }), []); + + useEffect(() => { + lastRenderedHtmlRef.current = null; + }, [props.envelope, props.html, props.protocolLines]); + useEffect(() => { if (!props.onEvent) return; return events.subscribe(() => { @@ -64,12 +147,19 @@ export function SummonSurface(props: SummonSurfaceProps) { const contract = props.capabilityRegistry?.toContract(); const componentContract = props.componentRegistry?.toContract(); const handlers = props.capabilityRegistry?.toPolicyHandlers() ?? {}; - const grantedIntents = props.capabilityRegistry?.intents() ?? []; - const grantedCapabilities = contract?.validationCapabilities ?? []; + const grantedIntents = props.grantedIntents ?? props.capabilityRegistry?.intents() ?? []; + const grantedCapabilities = props.grantedCapabilities ?? contract?.validationCapabilities ?? []; const initialState = { ...(contract?.initialState ?? {}), ...(props.initialState ?? {}), }; + const validationContext = validationContextFromProps( + props, + grantedIntents, + grantedCapabilities, + componentContract?.validationComponents ?? props.artifactComponents ?? props.envelope?.grants.components, + ); + validationContextRef.current = validationContext; let handle: SandboxHandle | null = null; let islands: ComponentIslandRegistry | null = props.componentRegistry @@ -92,10 +182,10 @@ export function SummonSurface(props: SummonSurfaceProps) { const artifact: Artifact = { // Advisory only. spawnSandbox receives host grants below. - intents: props.envelope?.grants.intents ?? [], - capabilities: props.envelope?.grants.capabilities, - components: props.envelope?.grants.components ?? componentContract?.validationComponents, - html: resolveHtml(props), + intents: props.artifactIntents ?? props.envelope?.grants.intents ?? grantedIntents, + capabilities: props.envelope?.grants.capabilities ?? props.grantedCapabilities, + components: props.artifactComponents ?? props.envelope?.grants.components ?? componentContract?.validationComponents, + html: resolveCompiledHtml(props, validationContext), initialState, }; const grantedComponentNames = new Set((artifact.components ?? []).map((component) => component.name)); @@ -110,11 +200,32 @@ export function SummonSurface(props: SummonSurfaceProps) { events, onSandboxFatal: props.onFatal, onIntent: (intent, args) => { - void policy.dispatch(intent, args); + props.onIntent?.(intent, args); + if (Object.prototype.hasOwnProperty.call(handlers, intent)) { + void policy.dispatch(intent, args); + } }, + onIntentRejected: props.onIntentRejected, onComponents: (components, sandboxId) => { + const grantedComponents = components.filter((component) => grantedComponentNames.has(component.name)); + for (const component of components) { + if (grantedComponentNames.has(component.name)) continue; + const error = { + code: 'unknown-component' as const, + sandboxId, + componentId: component.id, + componentName: component.name, + reason: `component "${component.name}" was not granted by the host`, + }; + events.push({ + kind: 'component-error', + at: Date.now(), + ...error, + }); + props.onComponentError?.(error); + } if (!islands && grantedComponentNames.size > 0) { - for (const component of components) { + for (const component of grantedComponents) { if (!grantedComponentNames.has(component.name)) continue; const error = { code: 'registry-missing' as const, @@ -132,7 +243,7 @@ export function SummonSurface(props: SummonSurfaceProps) { } return; } - islands?.sync(components, { + islands?.sync(grantedComponents, { sandboxId, emitIntent: (intent, args = {}) => { void policy.dispatch(intent, args); @@ -140,13 +251,26 @@ export function SummonSurface(props: SummonSurfaceProps) { }); }, }); + handleRef.current = handle; + preflightComponentProps( + artifact.html, + props.componentRegistry, + handle.sandboxId, + events, + props.onComponentError, + ); if (props.chrome) handle.setChrome(props.chrome); + if (lastRenderedHtmlRef.current !== null) { + handle.render(lastRenderedHtmlRef.current); + } return () => { islands?.destroy(); islands = null; handle?.dispose(); handle = null; + handleRef.current = null; + validationContextRef.current = null; }; }, [ events, @@ -155,8 +279,14 @@ export function SummonSurface(props: SummonSurfaceProps) { props.chrome, props.componentRegistry, props.envelope, + props.artifactIntents, + props.grantedIntents, + props.grantedCapabilities, + props.artifactComponents, props.html, props.initialState, + props.onIntent, + props.onIntentRejected, props.onFatal, props.onComponentError, props.onHandlerError, @@ -166,11 +296,12 @@ export function SummonSurface(props: SummonSurfaceProps) { return createElement('iframe', { ref: iframeRef, + id: props.id, title: props.title ?? 'Summon surface', className: props.className, style: props.style, }); -} +}); export interface ReactComponentDefinition extends Omit, 'render' | 'destroy'> { @@ -219,11 +350,120 @@ export function defineReactComponent( }; } -function resolveHtml(props: SummonSurfaceProps): string { - if (props.envelope) return props.envelope.html; - if (props.html !== undefined) return props.html; - if (!props.protocolLines) return ''; +function resolveCompiledHtml(props: SummonSurfaceProps, context: ValidationContext): CompiledArtifactHtml { + if (props.envelope) return props.envelope.compiledHtml; + if (props.html !== undefined) return compileForRender(props.html, context); + if (!props.protocolLines) return compileForRender('', context); const accumulator = new SectionAccumulator(); for (const line of props.protocolLines) accumulator.apply(line); - return accumulator.compose(); + return compileForRender(accumulator.compose(), context); +} + +function compileForRender(html: string, context: ValidationContext): CompiledArtifactHtml { + const result = compileArtifactHtml(html, context); + if (result.issues.some((issue) => issue.severity === 'block')) { + return '' as CompiledArtifactHtml; + } + return result.html; +} + +function compilePatchForRender( + patch: HtmlNodePatch, + context: ValidationContext, +): CompiledHtmlNodePatch | null { + const result = compileArtifactHtml(patch.html, { + ...context, + experimentalFragmentMode: 'html-node-v0', + }); + if (result.issues.some((issue) => issue.severity === 'block')) return null; + return { + sectionId: patch.sectionId, + nodeId: patch.nodeId, + ...(patch.parentId ? { parentId: patch.parentId } : {}), + html: result.html, + }; +} + +function preflightComponentProps( + html: CompiledArtifactHtml, + componentRegistry: ComponentRegistry | null | undefined, + sandboxId: string | undefined, + events: ReturnType, + onError: ((error: ComponentIslandError) => void) | undefined, +): void { + if (!componentRegistry || typeof DOMParser === 'undefined') return; + const doc = new DOMParser().parseFromString(`
${html}
`, 'text/html'); + const placeholders = doc.querySelectorAll('[data-summon-component]'); + for (const placeholder of placeholders) { + const componentName = placeholder.getAttribute('data-summon-component') ?? ''; + const componentId = placeholder.getAttribute('data-summon-component-id') ?? undefined; + const rawProps = placeholder.getAttribute('data-summon-props') ?? '{}'; + let props: unknown; + try { + props = JSON.parse(rawProps); + } catch { + emitComponentPreflightError({ + code: 'props-invalid', + componentId, + componentName, + sandboxId, + reason: `component "${componentName}" props are not valid JSON`, + }, events, onError); + continue; + } + const parsed = componentRegistry.validateProps(componentName, props); + if (parsed.ok) continue; + emitComponentPreflightError({ + code: parsed.error?.startsWith('unknown component') ? 'unknown-component' : 'props-invalid', + componentId, + componentName, + sandboxId, + reason: parsed.error ?? 'component props failed validation', + }, events, onError); + } +} + +function emitComponentPreflightError( + error: ComponentIslandError, + events: ReturnType, + onError: ((error: ComponentIslandError) => void) | undefined, +): void { + events.push({ + kind: 'component-error', + at: Date.now(), + code: error.code, + sandboxId: error.sandboxId, + componentId: error.componentId, + componentName: error.componentName, + reason: error.reason, + }); + onError?.(error); +} + +function validationContextFromProps( + props: SummonSurfaceProps, + grantedIntents: string[], + grantedCapabilities: Artifact['capabilities'], + components: Artifact['components'], +): ValidationContext { + const surfacePlan = props.envelope?.surfacePlan; + return { + mode: props.envelope?.metadata.mode ?? + (surfacePlan?.runtime === 'static' || grantedIntents.length === 0 ? 'static' : 'interactive'), + scriptPolicy: 'forbid', + allowedIntents: props.envelope?.grants.intents ?? grantedIntents, + capabilities: props.envelope?.grants.capabilities ?? grantedCapabilities, + components, + ...(surfacePlan ? { surfacePlan } : {}), + }; +} + +function defaultValidationContext(): ValidationContext { + return { + mode: 'static', + scriptPolicy: 'forbid', + allowedIntents: [], + capabilities: [], + components: [], + }; } diff --git a/packages/react/src/react-shim.d.ts b/packages/react/src/react-shim.d.ts index 9435204..b48e2fd 100644 --- a/packages/react/src/react-shim.d.ts +++ b/packages/react/src/react-shim.d.ts @@ -7,11 +7,22 @@ declare module 'react' { export interface RefObject { current: T | null; } + export type RefCallback = (instance: T | null) => void; + export type Ref = RefCallback | RefObject | null; + export type ForwardedRef = Ref; export function createElement( type: string | ComponentType, props: Record | null, ...children: ReactNode[] ): unknown; + export function forwardRef( + render: (props: P, ref: ForwardedRef) => ReactNode, + ): ComponentType

}>; + export function useImperativeHandle( + ref: Ref | undefined, + init: () => R, + deps?: readonly unknown[], + ): void; export function useEffect( effect: () => void | (() => void), deps?: readonly unknown[], diff --git a/packages/sandbox-runtime/src/bootstrap.js b/packages/sandbox-runtime/src/bootstrap.js index 41c8100..3cea847 100644 --- a/packages/sandbox-runtime/src/bootstrap.js +++ b/packages/sandbox-runtime/src/bootstrap.js @@ -1,7 +1,7 @@ // Summon sandbox bootstrap — runs FIRST inside every sandbox iframe, before any -// artifact HTML/JS. Installs window.sandbox (frozen) as the only way for the -// sandboxed code to talk to the host. Capture parent reference and the message -// constructor early so a later-injected script can't shadow them. +// compiled artifact HTML. Installs window.sandbox (frozen) as the trusted +// bridge for controlled test shells and legacy hosts. Generated artifacts do +// not receive executable scripts. (() => { 'use strict'; @@ -93,9 +93,13 @@ // ------------------------------------------------------------------------- let currentState = Object.freeze({}); + const localState = Object.create(null); const subscribers = new Set(); const mountedIntentKeys = new Set(); let componentSyncScheduled = false; + let componentSyncFallbackTimer = 0; + let componentLayoutPollTimer = 0; + let componentLayoutSignature = ''; let componentResizeObserver = null; const componentResizeObserved = new Set(); const SAFE_ATTR_BINDINGS = Object.freeze(['src', 'alt', 'title', 'aria-label', 'value', 'placeholder', 'disabled']); @@ -139,9 +143,9 @@ // function of (DOM, state); recomputing is idempotent and the cheapest // possible model. // - // Scripts written via `sandbox.onState` keep working. The contract is: a - // script subscriber should NOT mutate elements that carry a `data-summon-bind` - // / `-show` / `-hide` — the binder runs after subscribers and will overwrite. + // Generated scripts are not an artifact capability. The `window.sandbox` + // object remains a narrow trusted bridge for host-owned tests and legacy + // shells, but generated UI should express local behavior with attributes. function walkPath(obj, path) { if (!path) return obj; @@ -154,6 +158,17 @@ return cur; } + function hasPath(obj, path) { + if (!path) return true; + let cur = obj; + const parts = path.split('.'); + for (let i = 0; i < parts.length; i++) { + if (cur == null || !Object.prototype.hasOwnProperty.call(Object(cur), parts[i])) return false; + cur = cur[parts[i]]; + } + return true; + } + // Walk up to find the nearest ancestor (inclusive) with a matching foreach // scope name. Scope markers are JS properties on stamped clones — invisible // to the LLM-authored markup. @@ -216,6 +231,7 @@ const resource = findResourceScope(name, fromEl); return resourceStateValue(resource, rest); } + if (hasPath(localState, path)) return walkPath(localState, path); return walkPath(currentState, path); } @@ -247,6 +263,57 @@ return value; } + function seedLocalState(root) { + const hosts = root.querySelectorAll('[data-summon-local]'); + for (const host of hosts) { + const raw = host.getAttribute('data-summon-local') || ''; + if (!raw.trim()) continue; + let parsed; + try { parsed = JSON.parse(raw); } + catch (_) { continue; } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue; + for (const key in parsed) { + if (!Object.prototype.hasOwnProperty.call(parsed, key)) continue; + if (!/^[A-Za-z_$][\w$]{0,39}$/.test(key)) continue; + if (!Object.prototype.hasOwnProperty.call(localState, key)) { + localState[key] = parsed[key]; + } + } + } + } + + function parseConditionLiteral(raw) { + const trimmed = String(raw || '').trim(); + if (trimmed.length >= 2 && trimmed[0] === '"' && trimmed[trimmed.length - 1] === '"') { + try { return JSON.parse(trimmed); } catch (_) { return trimmed.slice(1, -1); } + } + if (trimmed.length >= 2 && trimmed[0] === "'" && trimmed[trimmed.length - 1] === "'") { + return trimmed.slice(1, -1); + } + return trimmed; + } + + function evalCondition(expr, fromEl) { + const raw = String(expr || '').trim(); + if (!raw) return false; + const match = raw.match(/^(!)?(\$?[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)(?:\s*(==|!=)\s*("[^"]*"|'[^']*'))?$/); + if (!match) return truthy(resolveKey(raw, fromEl)); + const negated = !!match[1]; + const path = match[2]; + const op = match[3]; + const literal = match[4]; + const value = resolveKey(path, fromEl); + let result; + if (op) { + const expected = parseConditionLiteral(literal); + result = String(value ?? '') === String(expected); + if (op === '!=') result = !result; + } else { + result = truthy(value); + } + return negated ? !result : result; + } + function applyResourceScopes(root) { const hosts = root.querySelectorAll('[data-summon-resource]'); for (const host of hosts) { @@ -304,7 +371,8 @@ const selector = '[data-summon-attr-' + attr + ']'; const els = root.querySelectorAll(selector); for (const el of els) { - const v = resolveKey(el.getAttribute('data-summon-attr-' + attr), el); + const raw = el.getAttribute('data-summon-attr-' + attr); + const v = attr === 'disabled' ? evalCondition(raw, el) : resolveKey(raw, el); applySafeAttribute(el, attr, v); } } @@ -348,9 +416,69 @@ } } + function applyClassBindings(root) { + const els = root.querySelectorAll('*'); + for (const el of els) { + for (const attr of Array.from(el.attributes)) { + if (!attr.name.startsWith('data-summon-class-')) continue; + const className = attr.name.slice('data-summon-class-'.length); + if (!/^[A-Za-z][\w-]{0,63}$/.test(className)) continue; + el.classList.toggle(className, evalCondition(attr.value, el)); + } + } + } + + function parseMotionEntries(value) { + const out = []; + for (const part of String(value || '').split(';')) { + const trimmed = part.trim(); + if (!trimmed) continue; + const bits = trimmed.split(':'); + if (bits.length !== 2) continue; + const phase = bits[0].trim(); + const recipe = bits[1].trim(); + if (!/^(enter|update)$/.test(phase)) continue; + if (!/^[a-z][a-z0-9-]{0,31}$/.test(recipe)) continue; + out.push({ phase, recipe }); + } + return out; + } + + function applyMotion(root) { + const motionEls = root.querySelectorAll('[data-summon-motion]'); + for (const el of motionEls) { + const entries = parseMotionEntries(el.getAttribute('data-summon-motion')); + for (const entry of entries) { + if (entry.phase === 'enter') { + el.classList.add('summon-motion-enter-' + entry.recipe); + } + } + } + const transitionEls = root.querySelectorAll('[data-summon-transition]'); + for (const el of transitionEls) { + const recipe = (el.getAttribute('data-summon-transition') || '').trim(); + if (/^[a-z][a-z0-9-]{0,31}$/.test(recipe)) { + el.classList.add('summon-transition-' + recipe); + } + } + } + + function triggerUpdateMotion(root) { + const motionEls = root.querySelectorAll('[data-summon-motion]'); + for (const el of motionEls) { + const entries = parseMotionEntries(el.getAttribute('data-summon-motion')); + for (const entry of entries) { + if (entry.phase === 'update') { + markTransientClass(el, 'summon-motion-update-' + entry.recipe, 560); + } + } + } + } + function applyBindings() { const root = document.getElementById('summon-root'); if (!root) return; + seedLocalState(root); // Resource scopes must exist before foreach resolution; foreach must stamp // clones before bind/show/hide queries run. applyResourceScopes(root); @@ -362,14 +490,16 @@ } const shows = root.querySelectorAll('[data-summon-show]'); for (const el of shows) { - el.hidden = !truthy(resolveKey(el.getAttribute('data-summon-show'), el)); + el.hidden = !evalCondition(el.getAttribute('data-summon-show'), el); } const hides = root.querySelectorAll('[data-summon-hide]'); for (const el of hides) { - el.hidden = truthy(resolveKey(el.getAttribute('data-summon-hide'), el)); + el.hidden = evalCondition(el.getAttribute('data-summon-hide'), el); } applyAttrBindings(root); - scheduleComponentSync(); + applyClassBindings(root); + applyMotion(root); + syncComponents(); } function parseArgs(raw) { @@ -384,10 +514,19 @@ function scheduleComponentSync() { if (componentSyncScheduled) return; componentSyncScheduled = true; - requestAnimationFrame(() => { + const run = () => { + if (!componentSyncScheduled) return; componentSyncScheduled = false; + if (componentSyncFallbackTimer) { + clearTimeout(componentSyncFallbackTimer); + componentSyncFallbackTimer = 0; + } syncComponents(); - }); + }; + if (typeof requestAnimationFrame === 'function') { + requestAnimationFrame(run); + } + componentSyncFallbackTimer = setTimeout(run, 50); } function syncComponents() { @@ -395,6 +534,7 @@ if (!root) return; const els = Array.from(root.querySelectorAll('[data-summon-component]')); const components = []; + const layoutParts = []; for (const el of els) { const name = el.getAttribute('data-summon-component') || ''; const id = el.getAttribute('data-summon-component-id') || ''; @@ -404,6 +544,7 @@ ? interpolate(parsed, el) : {}; const rect = el.getBoundingClientRect(); + layoutParts.push(componentLayoutPart(el, rect)); components.push({ id, name, @@ -417,7 +558,9 @@ }); if (components.length >= 64) break; } + componentLayoutSignature = layoutParts.join('|'); refreshComponentResizeObservers(root, els); + updateComponentLayoutPolling(els.length > 0); PARENT.postMessage({ type: 'SUMMON_COMPONENTS', sandbox_id: SANDBOX_ID, @@ -449,6 +592,46 @@ } } + function componentLayoutPart(el, rect) { + return [ + el.getAttribute('data-summon-component-id') || '', + el.getAttribute('data-summon-component') || '', + rect.left, + rect.top, + rect.width, + rect.height, + ].join(':'); + } + + function readComponentLayoutSignature() { + const root = document.getElementById('summon-root'); + if (!root) return ''; + const els = Array.from(root.querySelectorAll('[data-summon-component]')); + const parts = []; + for (const el of els.slice(0, 64)) { + parts.push(componentLayoutPart(el, el.getBoundingClientRect())); + } + return parts.join('|'); + } + + function updateComponentLayoutPolling(enabled) { + if (!enabled) { + if (componentLayoutPollTimer) { + clearInterval(componentLayoutPollTimer); + componentLayoutPollTimer = 0; + } + componentLayoutSignature = ''; + return; + } + if (componentLayoutPollTimer) return; + componentLayoutPollTimer = setInterval(() => { + const next = readComponentLayoutSignature(); + if (next === componentLayoutSignature) return; + componentLayoutSignature = next; + scheduleComponentSync(); + }, 100); + } + // Collect named form controls into a flat args object. Multi-step thinking: // - Inputs/selects/textareas with [name] → value. // - Checkboxes → boolean. @@ -493,6 +676,52 @@ emit(resource.name, args); } + function parseLocalValue(raw) { + const value = String(raw || '').trim(); + if (!value) return ''; + if (value === 'true') return true; + if (value === 'false') return false; + if (value === 'null') return null; + if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return Number(value); + if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') { + try { return JSON.parse(value); } catch (_) { return value.slice(1, -1); } + } + if (value.length >= 2 && value[0] === "'" && value[value.length - 1] === "'") { + return value.slice(1, -1); + } + return value; + } + + function applyLocalAction(el) { + const setValue = el.getAttribute('data-summon-set'); + const toggleValue = el.getAttribute('data-summon-toggle'); + let changed = false; + if (setValue) { + const idx = setValue.indexOf('='); + const key = idx === -1 ? '' : setValue.slice(0, idx).trim(); + const rawValue = idx === -1 ? '' : setValue.slice(idx + 1); + if (/^[A-Za-z_$][\w$]{0,39}$/.test(key)) { + const next = parseLocalValue(rawValue); + if (localState[key] !== next) { + localState[key] = next; + changed = true; + } + } + } + if (toggleValue) { + const key = toggleValue.trim(); + if (/^[A-Za-z_$][\w$]{0,39}$/.test(key)) { + localState[key] = !truthy(localState[key]); + changed = true; + } + } + if (!changed) return; + applyBindings(); + const root = document.getElementById('summon-root'); + if (root) triggerUpdateMotion(root); + scheduleComponentSync(); + } + function mountKeyFor(el, intent, rawArgs) { const section = el.closest('[data-summon-section]'); const sectionId = section ? section.getAttribute('data-summon-section') || 'root' : 'root'; @@ -534,6 +763,12 @@ document.addEventListener('click', (event) => { const t = event.target; if (!(t instanceof Element)) return; + const localEl = t.closest('[data-summon-set],[data-summon-toggle]'); + if (localEl) { + event.preventDefault(); + applyLocalAction(localEl); + return; + } const resourceEl = t.closest('[data-summon-resource-trigger="click"]'); if (resourceEl) { event.preventDefault(); @@ -611,9 +846,9 @@ * Otherwise (raw artifact HTML, no sections), fall back to a full * innerHTML replace. * - * Inline + + `); + const srcdoc = ` + + + + + + + +

+ `; + await page.locator('#sandbox').evaluate((iframe, html) => { + (iframe as HTMLIFrameElement).srcdoc = html; + }, srcdoc); + await expect.poll(async () => page.evaluate(() => { + const messages = (window as any).__summonMessages as any[]; + return messages.some((message) => message?.type === 'SUMMON_READY'); + })).toBe(true); + + async function patchNode(patch: any) { + await page.locator('#sandbox').evaluate((iframe, payload) => { + (iframe as HTMLIFrameElement).contentWindow?.postMessage({ + type: 'SUMMON_NODE_PATCH', + sandbox_id: payload.sandboxId, + patch: payload.patch, + }, '*'); + }, { patch, sandboxId }); + } + + await patchNode({ + sectionId: 'main', + nodeId: 'root', + html: '
', + }); + await patchNode({ + sectionId: 'main', + nodeId: 'a', + parentId: 'root', + html: '', + }); + await patchNode({ + sectionId: 'main', + nodeId: 'b', + parentId: 'root', + html: '

Draft

', + }); + + const sandbox = page.frameLocator('#sandbox'); + const input = sandbox.locator('[data-summon-node="a"] input'); + await expect(input).toBeVisible(); + await input.fill('sticky value'); + await expect(input).toBeFocused(); + + await patchNode({ + sectionId: 'main', + nodeId: 'b', + parentId: 'root', + html: '

Final

', + }); + + await expect(sandbox.locator('[data-summon-node="b"]')).toContainText('Final'); + await expect(input).toHaveValue('sticky value'); + await expect(input).toBeFocused(); + + await patchNode({ + sectionId: 'main', + nodeId: 'card', + parentId: 'root', + html: '

Sales

Loading
', + }); + const card = sandbox.locator('[data-summon-node="card"]'); + await expect(card).toHaveClass(/summon-node-enter/); + await expect(sandbox.locator('[data-summon-node="card"] [data-summon-node-children] > [data-summon-skeleton]')).toHaveCount(2); + + await patchNode({ + sectionId: 'main', + nodeId: 'card-value', + parentId: 'card', + html: '

Inside card

', + }); + + const cardSlot = sandbox.locator('[data-summon-node="card"] [data-summon-node-children]'); + const cardSlotChild = sandbox.locator( + '[data-summon-node="card"] [data-summon-node-children] > [data-summon-node="card-value"]', + ); + await expect(cardSlot).toHaveClass(/summon-slot-filled/); + await expect(sandbox.locator('[data-summon-node="card"] [data-summon-node-children] > [data-summon-skeleton]')).toHaveCount(0); + await expect(cardSlotChild).toContainText('Inside card'); + await expect(cardSlotChild).toHaveClass(/summon-node-enter/); + await expect(cardSlotChild).not.toHaveClass(/summon-node-enter/); + + await patchNode({ + sectionId: 'main', + nodeId: 'card', + parentId: 'root', + html: '

Sales updated

', + }); + + await expect(card).toHaveClass(/summon-node-update/); + await expect(card).toContainText('Sales updated'); + await expect(cardSlotChild).toContainText('Inside card'); + await expect(cardSlotChild).not.toHaveClass(/summon-node-update/); +}); + +test('sandbox render keeps generated scripts inert while local state and motion work', async ({ page }) => { + const sandboxId = 'local-state-test'; + await page.setContent(` + + + `); + const nonce = 'summonlocalnonce'; + const srcdoc = ` + + + + + + + +
+ `; + await page.locator('#sandbox').evaluate((iframe, html) => { + (iframe as HTMLIFrameElement).srcdoc = html; + }, srcdoc); + await expect.poll(async () => page.evaluate(() => { + const messages = (window as any).__summonMessages as any[]; + return messages.some((message) => message?.type === 'SUMMON_READY'); + })).toBe(true); + + const html = `
+ + +
Overview
+
Activity panel
+
Details
+
`; + await page.locator('#sandbox').evaluate((iframe, payload) => { + (iframe as HTMLIFrameElement).contentWindow?.postMessage({ + type: 'SUMMON_RENDER', + sandbox_id: payload.sandboxId, + html: payload.html, + }, '*'); + }, { sandboxId, html }); + + const sandbox = page.frameLocator('#sandbox'); + await expect(sandbox.locator('#overview')).toBeVisible(); + await expect(sandbox.locator('#activity-panel')).toBeHidden(); + await expect(sandbox.locator('#details')).toBeHidden(); + await sandbox.locator('#activity-tab').click(); + await expect(sandbox.locator('#activity-tab')).toHaveClass(/active/); + await expect(sandbox.locator('#overview')).toBeHidden(); + await expect(sandbox.locator('#activity-panel')).toBeVisible(); + await expect(sandbox.locator('#activity-panel')).toHaveClass(/summon-motion-enter-rise/); + await expect(sandbox.locator('#activity-panel')).toHaveClass(/summon-transition-fade-slide/); + await expect(sandbox.locator('#activity-panel')).toHaveClass(/summon-motion-update-pulse/); + await sandbox.locator('#toggle').click(); + await expect(sandbox.locator('#details')).toBeVisible(); + + const executed = await page.evaluate(() => { + const messages = (window as any).__summonMessages as any[]; + return messages.some((message) => message?.type === 'EXECUTED'); + }); + expect(executed).toBe(false); +}); + +test('generate showcase uses the agent broker by default', async ({ page }) => { let captured: any = null; await page.route('**/api/generate', async (route) => { captured = route.request().postDataJSON(); const body = jsonl([ - { op: 'meta', path: '/surface-policy', value: captured.surfacePolicy }, + { + op: 'meta', + path: '/agent-intent', + value: { + purpose: 'explore', + interaction: 'search', + dataNeed: 'host-resource', + sideEffect: 'none', + requestedCapabilities: ['search'], + requestedComponents: [], + confidence: 0.72, + rationale: 'deterministic keyword and catalog match', + }, + }, + { + op: 'meta', + path: '/agent-policy-resolution', + value: { + source: 'default', + intentSource: 'deterministic', + proposedSurfacePolicy: { + tier: 'declarative', + purpose: 'explore', + grants: ['search'], + components: [], + persistence: 'replayable', + }, + surfacePolicy: { + tier: 'declarative', + purpose: 'explore', + grants: ['search'], + persistence: 'replayable', + }, + rejectedCapabilities: [], + rejectedComponents: [], + fallback: false, + }, + }, + { op: 'meta', path: '/surface-policy', value: { tier: 'declarative', purpose: 'explore', grants: ['search'] } }, { op: 'meta', path: '/surface-plan', value: hostSearchPlan }, { op: 'set', path: '/screen', value: { sections: ['main'] } }, { @@ -130,20 +350,108 @@ test('generate showcase sends SurfacePolicy by default', async ({ page }) => { await route.fulfill({ status: 200, contentType: 'text/plain', body }); }); - await page.goto('/generate.html'); + await page.goto('/generate'); await page.locator('#scenario').selectOption('host-resource-search'); await page.locator('#go').click(); await expect(page.locator('#iframe-status')).toContainText('done'); expect(captured).toBeTruthy(); - expect(captured.surfacePolicy).toEqual({ - tier: 'declarative', - purpose: 'explore', - grants: ['search'], - }); + expect(captured.agent).toEqual({ enabled: true }); + expect(captured.surfacePolicy).toBeUndefined(); expect(captured.surfacePlan).toBeUndefined(); expect(captured.capabilities.intents.map((intent: any) => intent.name)).toEqual(['search']); expect(captured.scriptPolicy).toBe('forbid'); + await expect(page.locator('#contract-summary [data-contract-row="broker"]')).toContainText('default'); + await expect(page.locator('#log')).toContainText('agent policy'); +}); + +test('batch page brokers each generation request', async ({ page }) => { + const captured: any[] = []; + await page.route('**/api/generate', async (route) => { + const request = route.request().postDataJSON(); + captured.push(request); + const body = jsonl([ + { op: 'meta', path: '/agent-intent', value: { purpose: 'inform', interaction: 'none', dataNeed: 'embedded', sideEffect: 'none', requestedCapabilities: [], requestedComponents: [], confidence: 0.58 } }, + { op: 'meta', path: '/agent-policy-resolution', value: { source: 'default', intentSource: 'deterministic', surfacePolicy: { tier: 'static', purpose: 'inform', persistence: 'replayable' }, rejectedCapabilities: [], rejectedComponents: [], fallback: false } }, + { op: 'set', path: '/screen', value: { sections: ['main'] } }, + { op: 'add', path: '/section/main', html: '

Batch brokered

' }, + ]); + await route.fulfill({ status: 200, contentType: 'text/plain', body }); + }); + + await page.goto('/batch'); + await page.locator('#count').fill('1'); + await page.locator('#run').click(); + + await expect(page.locator('#summary')).toContainText('1 ok'); + expect(captured).toHaveLength(1); + expect(captured[0].agent).toEqual({ enabled: true }); + expect(captured[0].surfacePolicy).toBeUndefined(); + expect(captured[0].surfacePlan).toBeUndefined(); + await expect(page.locator('#grid')).toContainText('agent policy'); +}); + +test('fragment compare launches section and html node streams from the same prompt', async ({ page }) => { + const captured: any[] = []; + let releaseBoth!: () => void; + const bothArrived = new Promise((resolve) => { + releaseBoth = resolve; + }); + + await page.route('**/api/generate', async (route) => { + const request = route.request().postDataJSON(); + captured.push(request); + if (captured.length === 2) releaseBoth(); + await bothArrived; + + const body = request.fragmentMode === 'html-node-v0' + ? jsonl([ + { op: 'meta', path: '/agent-intent', value: { purpose: 'review', interaction: 'none', dataNeed: 'embedded', sideEffect: 'none', requestedCapabilities: [], requestedComponents: [], confidence: 0.58 } }, + { op: 'meta', path: '/agent-policy-resolution', value: { source: 'default', intentSource: 'deterministic', surfacePolicy: { tier: 'static', purpose: 'review', persistence: 'replayable' }, rejectedCapabilities: [], rejectedComponents: [], fallback: false } }, + { op: 'meta', path: '/experimental-fragments', value: { mode: 'html-node-v0' } }, + { op: 'set', path: '/screen', value: { sections: ['main'] } }, + { op: 'add', path: '/section/main/node/root', html: '
' }, + { op: 'add', path: '/section/main/node/card', parent: 'root', html: '

Node stream

' }, + { op: 'add', path: '/section/main/node/body', parent: 'card', html: '

Rendered as HTML node patches.

' }, + ]) + : jsonl([ + { op: 'meta', path: '/agent-intent', value: { purpose: 'review', interaction: 'none', dataNeed: 'embedded', sideEffect: 'none', requestedCapabilities: [], requestedComponents: [], confidence: 0.58 } }, + { op: 'meta', path: '/agent-policy-resolution', value: { source: 'default', intentSource: 'deterministic', surfacePolicy: { tier: 'static', purpose: 'review', persistence: 'replayable' }, rejectedCapabilities: [], rejectedComponents: [], fallback: false } }, + { op: 'set', path: '/screen', value: { sections: ['main'] } }, + { op: 'add', path: '/section/main', html: '

Section stream

Rendered as section fragments.

' }, + ]); + await route.fulfill({ status: 200, contentType: 'text/plain', body }); + }); + + await page.goto('/fragment-compare'); + await expect(page.locator('#prompt-preset-matrix')).toContainText('Operational workflows'); + await expect(page.locator('#prompt-preset-matrix')).toContainText('Complex'); + await page.getByRole('button', { name: /Operational workflows, Complex: Migration Control/ }).click(); + const prompt = await page.locator('#prompt').inputValue(); + expect(prompt).toContain('migration control room'); + await page.locator('#run').click(); + + await expect.poll(() => captured.length).toBe(2); + const sectionRequest = captured.find((request) => request.fragmentMode !== 'html-node-v0'); + const nodeRequest = captured.find((request) => request.fragmentMode === 'html-node-v0'); + expect(sectionRequest?.prompt).toBe(prompt); + expect(nodeRequest?.prompt).toBe(prompt); + expect(sectionRequest?.agent).toEqual({ enabled: true }); + expect(nodeRequest?.agent).toEqual({ enabled: true }); + expect(sectionRequest?.surfacePlan).toBeUndefined(); + expect(nodeRequest?.surfacePlan).toBeUndefined(); + expect(sectionRequest?.directionId).toBe(''); + expect(nodeRequest?.directionId).toBe(''); + expect(sectionRequest?.modelOptions).toEqual(nodeRequest?.modelOptions); + expect(sectionRequest?.modelOptions?.anthropicThinking).toBe('off'); + expect(sectionRequest?.fragmentMode).toBeUndefined(); + expect(nodeRequest?.fragmentMode).toBe('html-node-v0'); + + await expect(page.locator('#section-status')).toContainText('done'); + await expect(page.locator('#block-status')).toContainText('done'); + await expect(page.frameLocator('#section-frame').locator('body')).toContainText('Section stream'); + await expect(page.frameLocator('#block-frame').locator('body')).toContainText('Node stream'); + await expect(page.locator('#block-metrics')).toContainText('patches'); }); test('generate showcase sends raw SurfacePlan from the advanced override', async ({ page }) => { @@ -191,13 +499,13 @@ test('generate showcase sends raw SurfacePlan from the advanced override', async await route.fulfill({ status: 200, contentType: 'text/plain', body }); }); - await page.goto('/generate.html'); + await page.goto('/generate'); await expect(page.locator('#scenario')).toContainText('Validation Retry Diagnostics'); await page.locator('#scenario').selectOption('repair-diagnostics'); await page.locator('#token-preset').selectOption('accent-blue'); await expect(page.locator('#prompt')).toHaveValue(/onboarding checklist/); - await expect(page.locator('.scenario-card.active')).toContainText('Validation Retry Diagnostics'); + await expect(page.locator('[data-scenario-id="repair-diagnostics"][aria-pressed="true"]')).toContainText('Validation Retry Diagnostics'); await expect(page.locator('#repair-enabled')).toBeChecked(); await expect(page.locator('#custom-contract-panel')).toBeHidden(); await page.locator('#custom-contract-enabled').check(); @@ -275,12 +583,38 @@ test('generate loads Ghost root scenario and logs Ghost metadata', async ({ page }, { op: 'meta', - path: '/surface-policy', - value: captured.surfacePolicy, + path: '/agent-intent', + value: { + purpose: 'review', + interaction: 'select', + dataNeed: 'embedded', + sideEffect: 'local-state', + requestedCapabilities: ['choose'], + requestedComponents: [], + confidence: 0.72, + }, + }, + { + op: 'meta', + path: '/agent-policy-resolution', + value: { + source: 'default', + intentSource: 'deterministic', + surfacePolicy: { + tier: 'declarative', + purpose: 'review', + grants: ['choose'], + persistence: 'replayable', + }, + rejectedCapabilities: [], + rejectedComponents: [], + fallback: false, + }, }, + { op: 'meta', path: '/surface-policy', value: { tier: 'declarative', purpose: 'review', grants: ['choose'] } }, { op: 'meta', path: '/surface-plan', value: componentIslandsPlan }, { op: 'set', path: '/screen', value: { sections: ['main'] } }, - { op: 'add', path: '/section/main', html: '

Checkout Review

' }, + { op: 'add', path: '/section/main', html: '

Checkout Review

' }, { op: 'meta', path: '/ghost-token-source', @@ -320,15 +654,15 @@ test('generate loads Ghost root scenario and logs Ghost metadata', async ({ page await route.fulfill({ status: 200, contentType: 'text/plain', body }); }); - await page.goto('/generate.html'); - await expect(page.locator('#scenario')).toContainText('Ghost steer: checkout'); + await page.goto('/generate'); + await expect(page.locator('#scenario')).toContainText('Fingerprint: checkout'); await page.locator('#scenario').selectOption('ghost-checkout'); await page.locator('#go').click(); await expect(page.locator('#iframe-status')).toContainText('done'); - await expect(page.locator('#log')).toContainText('ghost context'); + await expect(page.locator('#log')).toContainText('fingerprint context'); await expect(page.locator('#log')).toContainText('Checkout Review'); - await expect(page.locator('#log')).toContainText('ghost review packet'); + await expect(page.locator('#log')).toContainText('fingerprint review packet'); expect(captured).toBeTruthy(); expect(captured.ghost).toEqual({ @@ -337,11 +671,8 @@ test('generate loads Ghost root scenario and logs Ghost metadata', async ({ page baseDirectionId: 'ghost', }); expect(captured.directionId).toBeUndefined(); - expect(captured.surfacePolicy).toEqual({ - tier: 'declarative', - purpose: 'review', - grants: ['choose'], - }); + expect(captured.agent).toEqual({ enabled: true }); + expect(captured.surfacePolicy).toBeUndefined(); expect(captured.surfacePlan).toBeUndefined(); expect(captured.capabilities.intents.map((intent: any) => intent.name)).toEqual(['choose']); }); @@ -361,6 +692,31 @@ test('component islands render in host overlay without widening the sandbox', as

Sandbox placeholder only

`; const body = jsonl([ + { + op: 'meta', + path: '/agent-intent', + value: { + purpose: 'review', + interaction: 'select', + dataNeed: 'embedded', + sideEffect: 'local-state', + requestedCapabilities: ['choose'], + requestedComponents: ['MetricCard', 'TrendSparkline', 'ApprovalStatus'], + confidence: 0.72, + }, + }, + { + op: 'meta', + path: '/agent-policy-resolution', + value: { + source: 'default', + intentSource: 'deterministic', + surfacePolicy: componentIslandsPolicy, + rejectedCapabilities: [], + rejectedComponents: [], + fallback: false, + }, + }, { op: 'meta', path: '/surface-plan', value: componentIslandsPlan }, { op: 'set', path: '/screen', value: { sections: ['main'] } }, { op: 'add', path: '/section/main', html }, @@ -382,10 +738,13 @@ test('component islands render in host overlay without widening the sandbox', as await route.fulfill({ status: 200, contentType: 'text/plain', body }); }); - await page.goto('/generate.html'); + await page.goto('/generate'); await page.locator('#scenario').selectOption('component-islands'); await page.locator('#go').click(); + await page.locator('#sandbox').scrollIntoViewIfNeeded(); + const sandbox = page.frameLocator('#sandbox'); + await expect(sandbox.locator('#sandbox-proof')).toContainText('Sandbox placeholder only'); const hostIsland = page.locator('[data-summon-component-id="launch-score"]'); await expect(hostIsland).toContainText('Launch score'); await expect(hostIsland).toContainText('84'); @@ -396,11 +755,11 @@ test('component islands render in host overlay without widening the sandbox', as 'TrendSparkline', 'ApprovalStatus', ]); - expect(captured.surfacePolicy).toEqual(componentIslandsPolicy); + expect(captured.agent).toEqual({ enabled: true }); + expect(captured.surfacePolicy).toBeUndefined(); expect(captured.surfacePlan).toBeUndefined(); expect(captured.scriptPolicy).toBe('forbid'); - const sandbox = page.frameLocator('#sandbox'); await expect(sandbox.locator('[data-summon-component-id="launch-score"]')).not.toContainText('84'); const iframe = await page.locator('#sandbox').elementHandle(); @@ -411,12 +770,6 @@ test('component islands render in host overlay without widening the sandbox', as await frame!.evaluate(() => window.scrollTo(0, 80)); await expect.poll(async () => (await hostIsland.boundingBox())?.y ?? 0).toBeLessThan(beforeScroll!.y - 40); - await frame!.evaluate(() => { - const el = document.querySelector('[data-summon-component-id="launch-score"]'); - if (el) el.style.height = '150px'; - }); - await expect.poll(async () => (await hostIsland.boundingBox())?.height ?? 0).toBeGreaterThan(130); - await page.evaluate(() => { window.postMessage({ type: 'SUMMON_COMPONENTS', @@ -466,10 +819,11 @@ test('component island prop failures do not render host DOM and emit diagnostics await route.fulfill({ status: 200, contentType: 'text/plain', body }); }); - await page.goto('/generate.html'); + await page.goto('/generate'); await page.locator('#scenario').selectOption('component-islands'); await page.locator('#go').click(); + await page.locator('#sandbox').scrollIntoViewIfNeeded(); await expect(page.locator('[data-summon-component-id="bad-props"]')).toHaveCount(0); await expect(page.locator('#devtools-log')).toContainText('component-error'); await expect(page.locator('#devtools-log')).toContainText('props-invalid');